├── .github
└── workflows
│ └── deploy-image.yml
├── .gitignore
├── Dockerfile
├── Dockerfile-Alpine
├── README.md
├── application.yaml.example
├── helm
└── tibber-pulse-reader
│ ├── Chart.yaml
│ ├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ └── deployment.yaml
│ └── values.yaml
├── pom.xml
└── src
├── main
├── java
│ └── de
│ │ └── wyraz
│ │ ├── sml
│ │ ├── AbstractSMLObject.java
│ │ ├── CRC16.java
│ │ ├── SMLGetListResponse.java
│ │ ├── SMLListEntry.java
│ │ ├── SMLMessage.java
│ │ ├── SMLMessageBody.java
│ │ ├── SMLMessageParser.java
│ │ ├── SMLPublicCloseResponse.java
│ │ ├── SMLPublicOpenResponse.java
│ │ ├── SMLTime.java
│ │ └── asn1
│ │ │ └── ASN1BERTokenizer.java
│ │ └── tibberpulse
│ │ ├── TibberPulseReader.java
│ │ ├── sink
│ │ ├── IMeterDataPublisher.java
│ │ ├── MQTTPublisher.java
│ │ ├── MeterDataHandler.java
│ │ ├── MeterReadingFilter.java
│ │ ├── OpenmetricsBuilder.java
│ │ └── OpenmetricsPushPublisher.java
│ │ ├── sml
│ │ ├── ObisNameMap.java
│ │ ├── SMLDecoder.java
│ │ └── SMLMeterData.java
│ │ ├── source
│ │ ├── MQTTSource.java
│ │ ├── PayloadEncoding.java
│ │ ├── TibberPulseHttpReader.java
│ │ └── TibberPulseSourceConfig.java
│ │ └── util
│ │ └── ByteUtil.java
└── resources
│ └── application.yaml
└── test
└── java
└── de
└── wyraz
├── sml
└── asn1
│ └── ASN1NumberDecoderTest.java
└── tibberpulse
├── sink
└── MeterReadingFilterTests.java
└── sml
├── LibSMLTests.java
└── SMLDecoderTests.java
/.github/workflows/deploy-image.yml:
--------------------------------------------------------------------------------
1 | # derived from https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions
2 | name: Create and publish Docker image
3 | on:
4 | push:
5 | branches:
6 | - master
7 | tags:
8 | - v*
9 |
10 | env:
11 | REGISTRY: ghcr.io
12 | IMAGE_NAME: ${{ github.repository_owner }}/tibber-pulse-reader
13 |
14 | jobs:
15 | build-and-push-image:
16 | runs-on: ubuntu-latest
17 | permissions:
18 | contents: read
19 | packages: write
20 |
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v3
24 |
25 | - name: Log in to the Container registry
26 | uses: docker/login-action@v2
27 | with:
28 | registry: ${{ env.REGISTRY }}
29 | username: ${{ github.actor }}
30 | password: ${{ secrets.GITHUB_TOKEN }}
31 |
32 | - name: Extract metadata (tags, labels) for Docker
33 | id: meta
34 | uses: docker/metadata-action@v4
35 | with:
36 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
37 |
38 | - name: Build with Maven
39 | run: mvn --batch-mode --update-snapshots package
40 |
41 | # - name: Upload jar to mega.nz
42 | # uses: Difegue/action-megacmd@043c6d2a167af3ae0904fc88c6b2829e4140dc8d
43 | # with:
44 | # args: put target/tibber-pulse-reader-1.0.0-SNAPSHOT.jar /tibber-pulse-reader/tibber-pulse-reader.${{ github.ref_name }}.jar
45 | # env:
46 | # USERNAME: ${{ secrets.MEGA_USERNAME }}
47 | # PASSWORD: ${{ secrets.MEGA_PASSWORD }}
48 |
49 | - name: Set up QEMU
50 | uses: docker/setup-qemu-action@v2
51 |
52 | - name: Set up Docker Buildx
53 | uses: docker/setup-buildx-action@v2
54 |
55 | - name: Build and push Docker image
56 | uses: docker/build-push-action@v4
57 | with:
58 | platforms: linux/amd64,linux/arm64
59 | context: .
60 | file: ./Dockerfile
61 | push: true
62 | tags: ${{ steps.meta.outputs.tags }}, ${{ env.REGISTRY }}/${{ github.repository }}:latest
63 | labels: ${{ steps.meta.outputs.labels }}
64 |
65 | - name: Build and push Docker image for Alpine
66 | uses: docker/build-push-action@v4
67 | with:
68 | platforms: linux/amd64
69 | context: .
70 | file: ./Dockerfile-Alpine
71 | push: true
72 | tags: ${{ steps.meta.outputs.tags }}-alpine
73 | labels: ${{ steps.meta.outputs.labels }}
74 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | /.classpath
3 | /.project
4 | /.settings/
5 | /application.yaml
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:17-bullseye
2 |
3 | ADD target/tibber-pulse-reader-1.0.0-SNAPSHOT.jar /tibber-pulse-reader.jar
4 |
5 | ENTRYPOINT ["java","-XX:+UnlockExperimentalVMOptions","-XX:+UseContainerSupport","-jar","/tibber-pulse-reader.jar"]
6 |
--------------------------------------------------------------------------------
/Dockerfile-Alpine:
--------------------------------------------------------------------------------
1 | FROM openjdk:17-alpine
2 |
3 | ADD target/tibber-pulse-reader-1.0.0-SNAPSHOT.jar /tibber-pulse-reader.jar
4 |
5 | ENTRYPOINT ["java","-XX:+UnlockExperimentalVMOptions","-XX:+UseContainerSupport","-jar","/tibber-pulse-reader.jar"]
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tibber Pulse Reader
2 |
3 | > :warning: **Not maintained anymore!** I have integrated the code into https://github.com/micw/homedatabroker. New features are added there.
4 |
5 | This tool reads out data from a Tibber Pulse locally (without roundtrip trough the cloud).
6 |
7 | This is an early version with poor documentation and a lot work-in-progress.
8 |
9 | ## Configuration
10 |
11 | * configure via environment variables (ideal to run in docker)
12 | * configure via `application.yaml` config file
13 | * see `application.yaml.example` for examples of config variables
14 |
15 | ## Running (docker)
16 |
17 | ```
18 | docker run -it --rm \
19 | -e "TIBBER_PULSE_SOURCE=http" \
20 | [...]
21 | ghcr.io/micw/tibber-pulse-reader:master
22 |
23 | ```
24 |
25 | * pass all config parameters as environment variables
26 | * alternatively, create an `application.yaml` and mount it to the docker container at `/application.yaml`
27 |
28 |
29 | To fetch the latest docker image, run:
30 |
31 | ```
32 | docker pull ghcr.io/micw/tibber-pulse-reader:master
33 | ```
34 |
35 | # Running (native)
36 |
37 | > **⚠ jars are not yet available for download**
38 |
39 | Pre-built jars can be downloaded from https://mega.nz/folder/F6x0WKjB#AIfMjKHa5gU_aWJEyhrP3w . To run it, you need a Java Runtime Environment (JRE) with version 11 or higher installed. Config can be passed as environment variables or by creating appliucation.yaml in the working directory (e.g. next to the downloaded jar file).
40 |
41 | Example:
42 |
43 | echo "TIBBER_PULSE_SOURCE: http" > application.yaml
44 | echo "TIBBER_PULSE_HOST: tibberbridge.localnet" >> application.yaml
45 | [...]
46 | java -Xmx25M -jar tibber-pulse-reader.master.jar
47 |
48 | Memory assignment of the process can be tuned by the -Xmx option - adjust it to your needs so that the process does not get an out of memory error.
49 |
50 | ## Modes of access
51 |
52 | ### HTTP access
53 |
54 | This access method allows to read data from an almost unmodified Tibber Pulse Bridge. Only the web interface must be enabled. A description how this can be done is in my blog article https://blog.wyraz.de/allgemein/a-brief-analysis-of-the-tibber-pulse-bridge/ .
55 |
56 | Configuration parameters:
57 |
58 | * `TIBBER_PULSE_SOURCE=http` (required) - enable the HTTP based access to Tibber Pulse Bridge
59 | * `TIBBER_PULSE_HOST` (required) - Hostname or IP address of the Tibber Pulse Bridge
60 | * `TIBBER_PULSE_NODE_ID` (default 1) - ID of the connected node (Tibber Pulse IR) - only required if more than one node is connected to the Tibber Pulse Bridge
61 | * `TIBBER_PULSE_USERNAME` (optional) - Username to access the Tibber Pulse Bridge (usually `admin`)
62 | * `TIBBER_PULSE_PASSWORD` (optional) - Password to access the Tibber Pulse Bridge (the initial password is printed on the QR-code of your Tibber Pulse Gateway)
63 | * `TIBBER_PULSE_CRON` (default `*/15 * * * * *`) - Cron expression how often the data should be read from Tibber Pulse Bridge. The default is every 15 seconds **A cron expression starting with an asterisk must be quoted when passed in a YAML file, otherwise the YAML syntax is invalid**
64 |
65 | ### MQTT access
66 |
67 | To access the MQTT data, the Tibber Pulse Bridge must be modified to talk to a local MQTT server. The local MQTT server must be configured to bridge to tibber's MQTT server. The process is described in https://github.com/MSkjel/LocalPulse2Tibber .
68 |
69 | Configuration parameters:
70 |
71 | * `TIBBER_PULSE_SOURCE=mqtt` (required) - enable the MQTT based access to Tibber Pulse Bridge
72 | * `MQTT_SOURCE_HOST` (required) - Hostname or IP address of the MQTT server to subscribe to
73 | * `MQTT_SOURCE_TLS` (default `false`) - set to true to use TLS/SSL for MQTT connection
74 | * `MQTT_SOURCE_PORT` (default `1883`) - Port of the MQTT server
75 | * `MQTT_SOURCE_USERNAME` (optional) - username for authentification if required by the MQTT server
76 | * `MQTT_SOURCE_PASSWORD` (optional) - password for authentification if required by the MQTT server
77 | * `MQTT_SOURCE_TOPIC` (required) - The topic to subscribe to and read SML from
78 | * `MQTT_SOURCE_PAYLOAD_ENCODING` (default `HEX`) - Encoding of the payload. Supported values: `HEX`, `BINARY`
79 |
80 | ## Publishers
81 |
82 | Publishers sends the decoded data to other systems like databases or message brokers. The followng publishers are available.
83 |
84 | ### MQTT publisher
85 |
86 | Publishes meter readings via MQTT. Each value is published on a separate topic. The topic and payload can be configured using placeholders.
87 |
88 | Configuration parameters:
89 |
90 | * `PUBLISH_MQTT_ENABLED` (required) - set to `true` to enable publishing over MQTT
91 | * `PUBLISH_MQTT_HOST` (required) - Hostname or IP address of the MQTT server to publish to
92 | * `PUBLISH_MQTT_TLS` (default `false`) - set to true to use TLS/SSL for MQTT connection
93 | * `PUBLISH_MQTT_PORT` (default 1883) - Port of the MQTT server
94 | * `PUBLISH_MQTT_USERNAME` (optional) - username for authentification if required by the MQTT server
95 | * `PUBLISH_MQTT_PASSWORD` (optional) - password for authentification if required by the MQTT server
96 | * `PUBLISH_MQTT_TOPIC`(default `{meterId}/{nameOrObisCode}`) - a template to build the topic to publish to. See below for allowed placeholders.
97 | * `PUBLISH_MQTT_VALUE`(default `{value}`) - a template to build the payload to publish. See below for allowed placeholders.
98 |
99 | #### Placeholders for topic and payload
100 |
101 | * `{meterId}`: The id of the meter like '01EBZ0123456789' or 'unknown' if no meter ID is received
102 | * `{obisCode}`: The OBIS code of the reading
103 | * `{name}`: A friendly name of the OBIS code like 'energyImportTotal' or blank if no name is known
104 | * `{nameOrObisCode}`: A friendly name of the OBIS code or the OBIS code if the name is not known
105 | * `{value}`: The numeric value of the reading
106 | * `{unit}`: The unit of the reading or blank if there is no unit
107 |
108 | ### OpenMetrics publisher
109 |
110 | Publishes metrics in [OpenMetrics format](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) via HTTP. Usefull to publish to [VictoriaMetrics](https://docs.victoriametrics.com/url-examples.html#apiv1importprometheus) or [Prometheus pushgateway](https://github.com/prometheus/pushgateway/blob/master/README.md).
111 |
112 | Configuration parameters:
113 |
114 | * `PUBLISH_OPENMETRICS_ENABLED` (required) - set to `true` to enable publishing over MQTT
115 | * `PUBLISH_OPENMETRICS_URL` (required) - URL of the endpoint to publish to
116 | * `PUBLISH_OPENMETRICS_USERNAME` (optional) - username for HTTP basic authentification
117 | * `PUBLISH_OPENMETRICS_PASSWORD` (optional) - password for HTTP basic authentification
118 |
119 |
120 | ## Other configuration
121 |
122 | * `LOG_LEVEL` (default `info`) - Log level to use by the application. Valid values are `debug`, `info`, `warn` and `error`.
123 | * run with `debug` to see all SML messages before/after decoding
124 | * `IGNORE_SML_CRC_ERRORS` (default `false`) - If set to true, an SML message containing CRC errors will still be processed. Only usefull for testing with problematic meters.
125 | * `PUBLISH_INTERVAL` (default empty) - Can be set to a cron expression to publish only one reading per interval.
126 |
127 | ### Filtering readings
128 |
129 | The config parameter `PUBLISH_FILTERS` allows to specify a list of filters applied to readings. Multiple filters are separated by whitespace or newline.
130 |
131 | A filter consists of a field selector (OBIS code or name) and a rule name (see below). Field names an rule names are not case sensitive.
132 |
133 | Example:
134 |
135 | Input:
136 | ```
137 | 1-0:1.8.0*255 / energyImportTotal = 123123.45 WATT_HOURS
138 | 1-0:2.8.0*255 / energyExportTotal = 345234.56 WATT_HOURS
139 | 1-0:16.7.0*255 / powerTotal = 11111.22 WATT
140 | 1-0:36.7.0*255 / powerL1 = 22333.33 WATT
141 | ```
142 |
143 | Filters:
144 | ```
145 | 1-0:1.8.0*255=IGNORE
146 | powerL1=IGNORE
147 | energyExportTotal=kWh
148 | powerL1=KILOWATT
149 | ```
150 |
151 | Result:
152 | ```
153 | 1-0:2.8.0*255 / energyExportTotal = 345.23456 KILOWATT_HOURS
154 | 1-0:16.7.0*255 / powerTotal = 11.11122 KILOWATT
155 | ```
156 |
157 | The following rules are allowed:
158 |
159 | * `IGNORE` - the reading will not be published
160 | * `KILOWATT` - if the reading is in WATT, it will be converted to KILOWATT
161 | * `kW` - alias for `KILOWATT`
162 | * `KILOWATT_HOURS` - if the reading is in WATT_HOURS, it will be converted to KILOWATT_HOURS
163 | * `kWh` - alias for `KILOWATT_HOURS`
164 |
--------------------------------------------------------------------------------
/application.yaml.example:
--------------------------------------------------------------------------------
1 | LOG_LEVEL: info
2 |
3 | TIBBER_PULSE_SOURCE: http
4 | TIBBER_PULSE_HOST: tibber_bridge.localnet
5 | TIBBER_PULSE_USERNAME: admin
6 | TIBBER_PULSE_PASSWORD: ABCD-1234
7 |
8 | # run every 15 seconds (default)
9 | # TIBBER_PULSE_CRON:*/15 * * * * *
10 |
11 | # enable and configure publishing via MQTT
12 | # PUBLISH_MQTT_ENABLED: true
13 | # PUBLISH_MQTT_HOST: mqtt.localnet
14 | # PUBLISH_MQTT_TLS: false
15 | # PUBLISH_MQTT_PORT: 1883
16 | # PUBLISH_MQTT_USERNAME: tibberdata
17 | # PUBLISH_MQTT_PASSWORD: secret
18 | # PUBLISH_MQTT_TOPIC: "{meterId}/{nameOrObisCode}"
19 | # PUBLISH_MQTT_VALUE: "{value}"
20 |
21 | # enable and configure publishing of OpenMetrics format to victoriametrics or prometheus push gateway
22 | # PUBLISH_OPENMETRICS_ENABLED: true
23 | # PUBLISH_OPENMETRICS_URL: https://vmserver.example.com/api/v1/import/prometheus
24 | # PUBLISH_OPENMETRICS_USERNAME: pushuser
25 | # PUBLISH_OPENMETRICS_PASSWORD: s3cr3t!
26 |
--------------------------------------------------------------------------------
/helm/tibber-pulse-reader/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: tibber-pulse-reader
3 |
4 | type: application
5 |
6 | version: 0.1.0
7 |
--------------------------------------------------------------------------------
/helm/tibber-pulse-reader/templates/NOTES.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/micw/tibber-pulse-reader/73247a11586f93787294ce33b35a11f305f17a36/helm/tibber-pulse-reader/templates/NOTES.txt
--------------------------------------------------------------------------------
/helm/tibber-pulse-reader/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "tibber-pulse-reader.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "tibber-pulse-reader.fullname" -}}
14 | {{- if .Values.fullnameOverride }}
15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default .Chart.Name .Values.nameOverride }}
18 | {{- if contains $name .Release.Name }}
19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 | {{/*
27 | Create chart name and version as used by the chart label.
28 | */}}
29 | {{- define "tibber-pulse-reader.chart" -}}
30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Common labels
35 | */}}
36 | {{- define "tibber-pulse-reader.labels" -}}
37 | helm.sh/chart: {{ include "tibber-pulse-reader.chart" . }}
38 | {{ include "tibber-pulse-reader.selectorLabels" . }}
39 | {{- if .Chart.AppVersion }}
40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41 | {{- end }}
42 | app.kubernetes.io/managed-by: {{ .Release.Service }}
43 | {{- end }}
44 |
45 | {{/*
46 | Selector labels
47 | */}}
48 | {{- define "tibber-pulse-reader.selectorLabels" -}}
49 | app.kubernetes.io/name: {{ include "tibber-pulse-reader.name" . }}
50 | app.kubernetes.io/instance: {{ .Release.Name }}
51 | {{- end }}
52 |
--------------------------------------------------------------------------------
/helm/tibber-pulse-reader/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ include "tibber-pulse-reader.fullname" . }}
5 | labels:
6 | {{- include "tibber-pulse-reader.labels" . | nindent 4 }}
7 | spec:
8 | replicas: 1
9 | selector:
10 | matchLabels:
11 | {{- include "tibber-pulse-reader.selectorLabels" . | nindent 6 }}
12 | template:
13 | metadata:
14 | {{- with .Values.podAnnotations }}
15 | annotations:
16 | {{- toYaml . | nindent 8 }}
17 | {{- end }}
18 | labels:
19 | {{- include "tibber-pulse-reader.selectorLabels" . | nindent 8 }}
20 | spec:
21 | {{- with .Values.imagePullSecrets }}
22 | imagePullSecrets:
23 | {{- toYaml . | nindent 8 }}
24 | {{- end }}
25 | securityContext:
26 | {{- toYaml .Values.podSecurityContext | nindent 8 }}
27 | containers:
28 | - name: {{ .Chart.Name }}
29 | securityContext:
30 | {{- toYaml .Values.securityContext | nindent 12 }}
31 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
32 | imagePullPolicy: {{ .Values.image.pullPolicy }}
33 | env:
34 | {{- if .Values.logLevel }}
35 | - name: LOG_LEVEL
36 | value: "{{ .Values.logLevel }}"
37 | {{- end }}
38 |
39 | - name: TIBBER_PULSE_SOURCE
40 | value: {{ .Values.pulse.source | required "pulse.source is required" }}
41 |
42 | {{- if eq .Values.pulse.source "http" }}
43 | - name: TIBBER_PULSE_HOST
44 | value: {{ .Values.pulse.host | required "pulse.host is required" }}
45 | {{- if .Values.pulse.username }}
46 | - name: TIBBER_PULSE_USERNAME
47 | value: {{ .Values.pulse.username }}
48 | - name: TIBBER_PULSE_PASSWORD
49 | value: {{ .Values.pulse.password | required "pulse.password is required if username is set" }}
50 | {{- end }}
51 | {{- if .Values.pulse.cron }}
52 | - name: TIBBER_PULSE_CRON
53 | value: "{{ .Values.pulse.cron }}"
54 | {{- end }}
55 | {{- end }}
56 |
57 | {{- if eq .Values.pulse.source "mqtt" }}
58 | - name: MQTT_SOURCE_HOST
59 | value: {{ .Values.pulse.host | required "pulse.host is required" }}
60 | {{- if .Values.pulse.username }}
61 | - name: MQTT_SOURCE_USERNAME
62 | value: {{ .Values.pulse.username }}
63 | - name: MQTT_SOURCE_PASSWORD
64 | value: {{ .Values.pulse.password | required "pulse.password is required if username is set" }}
65 | {{- end }}
66 | - name: MQTT_SOURCE_TOPIC
67 | value: {{ .Values.pulse.topic | required "pulse.topic is required" }}
68 | {{- end }}
69 |
70 | {{- if .Values.publish.filters }}
71 | - name: PUBLISH_FILTERS
72 | value: {{ .Values.publish.filters | quote }}
73 | {{- end }}
74 |
75 | {{- if .Values.publish.interval }}
76 | - name: PUBLISH_INTERVAL
77 | value: {{ .Values.publish.interval | quote }}
78 | {{- end }}
79 |
80 | {{- if .Values.publish.mqtt.enabled }}
81 | - name: PUBLISH_MQTT_ENABLED
82 | value: "true"
83 | - name: PUBLISH_MQTT_HOST
84 | value: {{ .Values.publish.mqtt.host | required "pulse.publish.mqtt is required" }}
85 | - name: PUBLISH_MQTT_PORT
86 | value: "{{ .Values.publish.mqtt.port | default 1883 }}"
87 | {{- if .Values.publish.mqtt.username }}
88 | - name: PUBLISH_MQTT_USERNAME
89 | value: {{ .Values.publish.mqtt.username }}
90 | - name: PUBLISH_MQTT_PASSWORD
91 | value: {{ .Values.publish.mqtt.password | required "publish.mqtt.password is required if username is set" }}
92 | {{- end }}
93 | {{- if .Values.publish.mqtt.topic }}
94 | - name: PUBLISH_MQTT_TOPIC
95 | value: {{ .Values.publish.mqtt.topic | quote }}
96 | {{- end }}
97 | {{- if .Values.publish.mqtt.payload }}
98 | - name: PUBLISH_MQTT_PAYLOAD
99 | value: {{ .Values.publish.mqtt.payload | quote }}
100 | {{- end }}
101 | {{- end }}
102 |
103 | {{- if .Values.publish.openmetrics.enabled }}
104 | - name: PUBLISH_OPENMETRICS_ENABLED
105 | value: "true"
106 | - name: PUBLISH_OPENMETRICS_URL
107 | value: {{ .Values.publish.openmetrics.url | required "pulse.openmetrics.url is required" }}
108 | {{- if .Values.publish.openmetrics.username }}
109 | - name: PUBLISH_OPENMETRICS_USERNAME
110 | value: {{ .Values.publish.openmetrics.username }}
111 | - name: PUBLISH_OPENMETRICS_PASSWORD
112 | value: {{ .Values.publish.openmetrics.password | required "publish.openmetrics.password is required if username is set" }}
113 | {{- end }}
114 | {{- end }}
115 | resources:
116 | {{- toYaml .Values.resources | nindent 12 }}
117 | {{- with .Values.nodeSelector }}
118 | nodeSelector:
119 | {{- toYaml . | nindent 8 }}
120 | {{- end }}
121 | {{- with .Values.affinity }}
122 | affinity:
123 | {{- toYaml . | nindent 8 }}
124 | {{- end }}
125 | {{- with .Values.tolerations }}
126 | tolerations:
127 | {{- toYaml . | nindent 8 }}
128 | {{- end }}
129 |
--------------------------------------------------------------------------------
/helm/tibber-pulse-reader/values.yaml:
--------------------------------------------------------------------------------
1 | # Default values for tibber-pulse-reader.
2 | # This is a YAML-formatted file.
3 | # Declare variables to be passed into your templates.
4 |
5 | pulse:
6 | source: http
7 | # host: 192.168.1.2
8 | # username: admin
9 | # password: ABC-DEF
10 | # cron: "*/15 * * * * *"
11 |
12 | # source: mqtt
13 | # host: 192.168.1.3
14 | # username: mqttuser
15 | # password: mqttpass
16 | # topic: values/sml
17 |
18 | publish:
19 | # filters: |
20 | # energyImportTotal=kWh
21 | # energyExportTotal=kWh
22 | # energyImportTariff1=ignore
23 | # energyImportTariff2=ignore
24 | # interval: "0/15 * * * * *"
25 | mqtt:
26 | enabled: false
27 | #host: 192.168.1.3
28 | #port: 1883
29 | #username: user
30 | #password: pass
31 | #topic: "{meterId}/{nameOrObisCode}"
32 | #payload: "{value}"
33 | openmetrics:
34 | enabled: false
35 | #url: https://vmserver.example.com/api/v1/import/prometheus
36 | #username: user
37 | #password: pass
38 |
39 | #logLevel: debug
40 |
41 | image:
42 | repository: ghcr.io/micw/tibber-pulse-reader
43 | pullPolicy: Always
44 | tag: master
45 |
46 | nameOverride: ""
47 | fullnameOverride: ""
48 |
49 | podAnnotations: {}
50 |
51 | podSecurityContext: {}
52 | # fsGroup: 2000
53 |
54 | securityContext: {}
55 | # capabilities:
56 | # drop:
57 | # - ALL
58 | # readOnlyRootFilesystem: true
59 | # runAsNonRoot: true
60 | # runAsUser: 1000
61 |
62 | resources:
63 | # We usually recommend not to specify default resources and to leave this as a conscious
64 | # choice for the user. This also increases chances charts run on environments with little
65 | # resources, such as Minikube. If you do want to specify resources, uncomment the following
66 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
67 | limits:
68 | # cpu: 100m
69 | memory: 100Mi
70 | requests:
71 | # cpu: 100m
72 | memory: 100Mi
73 |
74 | nodeSelector: {}
75 |
76 | tolerations: []
77 |
78 | affinity: {}
79 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 | de.wyraz
4 | tibber-pulse-reader
5 | 1.0.0-SNAPSHOT
6 |
7 | org.springframework.boot
8 | spring-boot-starter-parent
9 | 2.5.14
10 |
11 |
12 |
13 | 11
14 | de.wyraz.tibberpulse.TibberPulseReader
15 |
16 |
17 |
18 | org.springframework.boot
19 | spring-boot-starter
20 |
21 |
22 | org.apache.httpcomponents
23 | httpclient
24 |
25 |
26 | org.apache.commons
27 | commons-lang3
28 |
29 |
30 | org.openmuc
31 | jsml
32 | 1.1.2
33 |
34 |
35 | org.eclipse.paho
36 | org.eclipse.paho.client.mqttv3
37 | 1.2.0
38 |
39 |
40 | org.bouncycastle
41 | bcprov-jdk18on
42 | 1.72
43 |
44 |
45 |
46 |
47 | org.assertj
48 | assertj-core
49 | test
50 |
51 |
52 | junit
53 | junit
54 | test
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | org.springframework.boot
63 | spring-boot-maven-plugin
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/sml/AbstractSMLObject.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.sml;
2 |
3 | import java.lang.reflect.Field;
4 | import java.util.List;
5 |
6 | import de.wyraz.tibberpulse.util.ByteUtil;
7 |
8 | public abstract class AbstractSMLObject {
9 |
10 | public String toString() {
11 | StringBuilder sb=new StringBuilder();
12 | appendToString(sb, 0, true);
13 | return sb.toString();
14 | }
15 |
16 | protected void appendToString(StringBuilder sb, int indent, boolean printClassName) {
17 | if (printClassName) {
18 | indentAppend(sb, indent, getClass().getSimpleName());
19 | indent++;
20 | }
21 | for (Field field: getClass().getDeclaredFields()) {
22 | field.setAccessible(true);
23 | Object value;
24 | try {
25 | value=field.get(this);
26 | } catch (Exception ex) {
27 | value=ex.toString();
28 | }
29 | indentAppend(sb, indent, field.getName(), value);
30 | }
31 | }
32 |
33 | protected void indentAppend(StringBuilder sb, int indent, String name, Object value) {
34 |
35 | if (value instanceof List) {
36 | List> list=(List>) value;
37 | indentAppend(sb, indent, name+" = List["+list.size()+"]");
38 | indent++;
39 | for (int i=0;i0) {
60 | sb.append("\n");
61 | }
62 | for (int i=0;i> 8) ^ CRC16_TABLE[(result ^ buffer[i]) & 0xff];
31 | }
32 | result ^= 0xffff;
33 | result = ((result & 0xff) << 8) | ((result & 0xff00) >> 8);
34 | return result;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/sml/SMLGetListResponse.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.sml;
2 |
3 | import java.util.List;
4 |
5 | public class SMLGetListResponse extends SMLMessageBody {
6 |
7 | protected byte[] clientId;
8 | protected byte[] serverId;
9 | protected byte[] listName;
10 | protected SMLTime actSensorTime;
11 | protected List valList;
12 | protected byte[] listSignature;
13 | protected SMLTime actGatewayTime;
14 |
15 | public byte[] getClientId() {
16 | return clientId;
17 | }
18 | public byte[] getServerId() {
19 | return serverId;
20 | }
21 | public byte[] getListName() {
22 | return listName;
23 | }
24 | public SMLTime getActSensorTime() {
25 | return actSensorTime;
26 | }
27 | public List getValList() {
28 | return valList;
29 | }
30 | public byte[] getListSignature() {
31 | return listSignature;
32 | }
33 | public SMLTime getActGatewayTime() {
34 | return actGatewayTime;
35 | }
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/sml/SMLListEntry.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.sml;
2 |
3 | import org.openmuc.jsml.EUnit;
4 |
5 | public class SMLListEntry extends AbstractSMLObject {
6 |
7 | protected byte[] objName;
8 | protected Long status;
9 | protected SMLTime valTime;
10 | protected EUnit valUnit;
11 | protected Number scaler;
12 | protected Object value;
13 | protected byte[] valueSignature;
14 |
15 | public byte[] getObjName() {
16 | return objName;
17 | }
18 | public Long getStatus() {
19 | return status;
20 | }
21 | public SMLTime getValTime() {
22 | return valTime;
23 | }
24 | public EUnit getValUnit() {
25 | return valUnit;
26 | }
27 | public Number getScaler() {
28 | return scaler;
29 | }
30 | public Object getValue() {
31 | return value;
32 | }
33 | public byte[] getValueSignature() {
34 | return valueSignature;
35 | }
36 |
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/sml/SMLMessage.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.sml;
2 |
3 | public class SMLMessage extends AbstractSMLObject {
4 |
5 | protected byte[] transactionId;
6 | protected Integer groupNo;
7 | protected Integer abortOnError;
8 | protected SMLMessageBody messageBody;
9 | protected Integer crc16Actual;
10 | protected Integer crc16Expected;
11 | protected Boolean crc16Ok;
12 |
13 | public byte[] getTransactionId() {
14 | return transactionId;
15 | }
16 | public Integer getGroupNo() {
17 | return groupNo;
18 | }
19 | public Integer getAbortOnError() {
20 | return abortOnError;
21 | }
22 | public SMLMessageBody getMessageBody() {
23 | return messageBody;
24 | }
25 | public Integer getCrc16Actual() {
26 | return crc16Actual;
27 | }
28 | public Integer getCrc16Expected() {
29 | return crc16Expected;
30 | }
31 | public Boolean getCrc16Ok() {
32 | return crc16Ok;
33 | }
34 |
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/sml/SMLMessageBody.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.sml;
2 |
3 | public abstract class SMLMessageBody extends AbstractSMLObject {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/sml/SMLMessageParser.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.sml;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Collection;
5 | import java.util.List;
6 |
7 | import org.openmuc.jsml.EUnit;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | import de.wyraz.sml.SMLTime.SMLTimeSecIndex;
12 | import de.wyraz.sml.asn1.ASN1BERTokenizer;
13 | import de.wyraz.sml.asn1.ASN1BERTokenizer.Type;
14 | import de.wyraz.tibberpulse.util.ByteUtil;
15 |
16 | /**
17 | * SML message parser, based on
18 | * https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03109/TR-03109-1_Anlage_Feinspezifikation_Drahtgebundene_LMN-Schnittstelle_Teilb.pdf?__blob=publicationFile&v=1
19 | *
20 | * References in comments are to the document above.
21 | *
22 | * License: AGPLv3
23 | *
24 | * @author mwyraz
25 | */
26 | public class SMLMessageParser {
27 |
28 | protected final Logger log = LoggerFactory.getLogger(getClass());
29 |
30 | public static Collection parse(byte[] payload) {
31 | return new SMLMessageParser(payload).parseSMLMessages();
32 | }
33 |
34 | protected final ASN1BERTokenizer tokenizer;
35 |
36 | protected SMLMessageParser(byte[] payload) {
37 | tokenizer=new ASN1BERTokenizer(payload);
38 | }
39 |
40 | protected Collection parseSMLMessages() {
41 |
42 | List results=new ArrayList<>();
43 |
44 | while (tokenizer.hasMoreData()) {
45 |
46 | tokenizer.readListOfElements(6, false);
47 |
48 | int crcStartOffset=tokenizer.getOffset();
49 |
50 | SMLMessage result = new SMLMessage();
51 | result.transactionId = tokenizer.readOctetString(false);
52 | result.groupNo = tokenizer.readUnsigned8(false);
53 | result.abortOnError = tokenizer.readUnsigned8(false);
54 | result.messageBody = parseSMLMessageBody();
55 | result.crc16Actual = CRC16.getCrc16(tokenizer.getMessage(), crcStartOffset, tokenizer.getOffset()-1);
56 | result.crc16Expected = tokenizer.readUnsigned16(false);
57 | result.crc16Ok = result.crc16Actual.equals(result.crc16Expected);
58 |
59 | tokenizer.readEndOfMessage(false);
60 |
61 | results.add(result);
62 | }
63 |
64 | return results;
65 | }
66 |
67 |
68 | protected SMLMessageBody parseSMLMessageBody() {
69 | long choice = readChoice32();
70 |
71 | if (choice == 0x00000101) {
72 | return parseSMLPublicOpenResponse();
73 | }
74 | if (choice == 0x00000201) {
75 | return parseSMLPublicCloseResponse();
76 | }
77 | if (choice == 0x00000701) {
78 | return parseSMLGetListResponse();
79 | }
80 |
81 | throw new RuntimeException("Unimplemented SML message body: " + ByteUtil.int32ToHex(choice));
82 | }
83 |
84 | protected SMLPublicOpenResponse parseSMLPublicOpenResponse() {
85 | tokenizer.readListOfElements(6, false);
86 |
87 | SMLPublicOpenResponse result = new SMLPublicOpenResponse();
88 | result.codepage = tokenizer.readOctetString(true);
89 | result.clientId = tokenizer.readOctetString(true);
90 | result.reqFileId = tokenizer.readOctetString(true);
91 | result.serverId = tokenizer.readOctetString(true);
92 | result.refTime = parseSMLTime(true);
93 | result.smlVersion = tokenizer.readUnsigned8(true);
94 |
95 | return result;
96 | }
97 |
98 | protected SMLPublicCloseResponse parseSMLPublicCloseResponse() {
99 | tokenizer.readListOfElements(1, false);
100 |
101 | SMLPublicCloseResponse result = new SMLPublicCloseResponse();
102 | result.globalSignature = tokenizer.readOctetString(true);
103 |
104 | return result;
105 | }
106 |
107 | protected SMLGetListResponse parseSMLGetListResponse() {
108 |
109 | tokenizer.readListOfElements(7, false);
110 |
111 | SMLGetListResponse result = new SMLGetListResponse();
112 | result.clientId = tokenizer.readOctetString(true);
113 | result.serverId = tokenizer.readOctetString(true);
114 | result.listName = tokenizer.readOctetString(true);
115 | result.actSensorTime = parseSMLTime(true);
116 |
117 | int elementCount=tokenizer.readListOfElements(-1, false);
118 | result.valList = parseSMLList(elementCount);
119 |
120 | result.listSignature = tokenizer.readOctetString(true);
121 | result.actGatewayTime = parseSMLTime(true);
122 |
123 | return result;
124 |
125 | }
126 |
127 | protected List parseSMLList(int elementCount) {
128 | List result=new ArrayList<>(elementCount);
129 | for (int i=0;i negative number
143 | // add a "00" byte so that BigInteger sees a larger positive number
144 | byte[] bytes=new byte[length + 1];
145 | bytes[0] = 0;
146 | System.arraycopy(data, offset, bytes, 1, length);
147 | return new BigInteger(bytes);
148 | }
149 |
150 | return new BigInteger(data, offset, length);
151 | }
152 |
153 | public static Number decodeSigned(byte[] data, int offset, int length) {
154 |
155 | if (length==1) {
156 | return data[offset];
157 | }
158 |
159 | if (length>1 && length<=4) {
160 | return new BigInteger(data, offset, length).intValue();
161 | }
162 |
163 | if (length>4 && length<=8) {
164 | return new BigInteger(data, offset, length).longValue();
165 | }
166 |
167 |
168 | return new BigInteger(data, offset, length);
169 | }
170 |
171 | public boolean hasMoreData() {
172 | return offset-1 && expectedSize!=dataLength) {
226 | throw new RuntimeException("Expected "+expectedType+" of length "+expectedSize+" but found "+type.describe(typeValue, dataLength, object));
227 | }
228 | return object;
229 | }
230 |
231 |
232 |
233 | public Type readNext() {
234 |
235 | this.typeValue = 0;
236 | this.type = Type.UNKNOWN;
237 | this.dataLength = 0;
238 | this.object = null;
239 |
240 | if (!hasMoreData()) {
241 | if (this.type == Type.END_OF_FILE) {
242 | throw new RuntimeException("Read after END_OF_FILE");
243 | }
244 | this.type = Type.END_OF_FILE;
245 | return this.type;
246 | }
247 |
248 | byte tlField = message[offset++];
249 | this.typeValue =(byte) (tlField & 0b01110000);
250 |
251 | if (tlField == 0x0) {
252 | this.type = Type.END_OF_MESSAGE;
253 | return this.type;
254 | }
255 |
256 | int tlLength=1;
257 | int tlAndDataLength=(tlField & 0b1111);
258 | while ((tlField & 0b10000000)!=0) {
259 | tlField = message[offset++];
260 | tlAndDataLength = (((tlAndDataLength & 0xffffffff ) << 4) | (tlField & 0b00001111));
261 | tlLength++;
262 | }
263 |
264 | this.dataLength=tlAndDataLength-tlLength;
265 |
266 | switch (this.typeValue) {
267 | case 0b01110000:
268 | this.type = Type.LIST;
269 | this.dataLength = tlAndDataLength; // since length is not in bytes, tlLength is not substracted
270 | break;
271 | case 0b01100000:
272 | this.type = Type.UNSIGNED;
273 | this.object = decodeUnsigned(message,offset,this.dataLength);
274 | offset+=this.dataLength;
275 | break;
276 | case 0b01010000:
277 | this.type = Type.SIGNED;
278 | this.object = decodeSigned(message,offset,this.dataLength);
279 | offset+=this.dataLength;
280 | break;
281 | case 0b00000000:
282 | if (this.dataLength==0) {
283 | this.type = Type.NULL;
284 | break;
285 | }
286 | this.type = Type.OCTET_STRING;
287 | // no "break", same
288 | default:
289 | this.object = Arrays.copyOfRange(message, offset, offset+this.dataLength);
290 | offset+=this.dataLength;
291 | break;
292 | }
293 |
294 | return this.type;
295 | }
296 |
297 | public void dump(PrintStream out) {
298 | dump(out, 0, Integer.MAX_VALUE);
299 | }
300 |
301 | protected void dump(PrintStream out, int depth, int maxElementCount) {
302 | while ((maxElementCount--)>0 && readNext()!=Type.END_OF_FILE) {
303 |
304 | for (int i=0;i rules;
133 |
134 | public MeterReadingFilter(String specs) {
135 | rules=new HashMap<>();
136 | for (String spec: specs.split("[\\r\\n\\s]+")) {
137 | if (spec.isEmpty()) {
138 | continue;
139 | }
140 | int eqPos=spec.indexOf("=");
141 | if (eqPos<1 || eqPos==spec.length()-1) {
142 | throw new IllegalArgumentException("Invalid filter spec: "+spec);
143 | }
144 | String key=spec.substring(0,eqPos).toLowerCase();
145 | Rule rule=Rule.find(spec.substring(eqPos+1));
146 |
147 | rules.put(key, rule);
148 | }
149 | }
150 |
151 | public List apply(List input) {
152 |
153 | List result=new ArrayList<>();
154 |
155 | for (SMLMeterData.Reading r: input) {
156 | Rule rule=rules.get(r.getObisCode().toLowerCase());
157 | if (rule==null && r.getName()!=null) {
158 | rule=rules.get(r.getName().toLowerCase());
159 | }
160 | if (rule==null) { // no filtering
161 | result.add(r);
162 | } else {
163 | r=rule.apply(r); // filter the reading
164 | if (r!=null) {
165 | result.add(r);
166 | }
167 | }
168 |
169 | }
170 |
171 | return result;
172 | }
173 |
174 | }
175 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/sink/OpenmetricsBuilder.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.sink;
2 |
3 | import java.time.ZonedDateTime;
4 |
5 | import org.apache.commons.lang3.StringUtils;
6 |
7 | /**
8 | * Builds a metric as described in https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md
9 | * @author mwyraz
10 | *
11 | */
12 | public class OpenmetricsBuilder {
13 |
14 | protected StringBuilder sb;
15 |
16 | public OpenmetricsBuilder() {
17 | sb=new StringBuilder();
18 | }
19 |
20 | public MetricBuilder metric(String metric) {
21 | return new MetricBuilder(metric);
22 | }
23 |
24 | protected void appendMetricLine(CharSequence line) {
25 | if (sb.length()>0) {
26 | sb.append("\n");
27 | }
28 | sb.append(line);
29 | }
30 |
31 | public class MetricBuilder {
32 | protected StringBuilder sb;
33 | protected Long timestamp;
34 | protected boolean hasTags;
35 | protected MetricBuilder(String metric) {
36 | sb=new StringBuilder();
37 | sb.append(metric);
38 | }
39 |
40 | public MetricBuilder timestamp(ZonedDateTime timestamp) {
41 | if (timestamp==null) {
42 | this.timestamp=null;
43 | } else {
44 | this.timestamp=timestamp.toEpochSecond();
45 | }
46 | return this;
47 | }
48 |
49 | public MetricBuilder tag(String key, String value) {
50 | if (!StringUtils.isAnyBlank(key,value)) {
51 | if (hasTags) {
52 | sb.append(",");
53 | } else {
54 | sb.append(" {");
55 | hasTags=true;
56 | }
57 | sb.append(key);
58 | sb.append("=\"");
59 | sb.append(value); // FIXME: proper escaping!
60 | sb.append("\"");
61 | }
62 | return this;
63 | }
64 |
65 |
66 | public OpenmetricsBuilder value(Number value) {
67 | if (value!=null) {
68 | if (hasTags) {
69 | sb.append("}");
70 | }
71 | sb.append(" ").append(value);
72 | if (timestamp!=null) {
73 | sb.append(" ").append(timestamp);
74 | }
75 |
76 | appendMetricLine(sb);
77 | }
78 | return OpenmetricsBuilder.this;
79 | }
80 | }
81 |
82 | public String build() {
83 | String result=sb.toString();
84 | sb.setLength(0);
85 | return result;
86 | }
87 |
88 |
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/sink/OpenmetricsPushPublisher.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.sink;
2 |
3 | import java.io.IOException;
4 | import java.nio.charset.StandardCharsets;
5 |
6 | import org.apache.commons.codec.binary.Base64;
7 | import org.apache.commons.lang3.StringUtils;
8 | import org.apache.http.client.methods.CloseableHttpResponse;
9 | import org.apache.http.client.methods.RequestBuilder;
10 | import org.apache.http.entity.StringEntity;
11 | import org.apache.http.impl.client.CloseableHttpClient;
12 | import org.apache.http.impl.client.HttpClients;
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 | import org.springframework.beans.factory.annotation.Value;
16 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
17 | import org.springframework.stereotype.Service;
18 |
19 | import de.wyraz.tibberpulse.sml.SMLMeterData;
20 | import de.wyraz.tibberpulse.sml.SMLMeterData.Reading;
21 |
22 | /**
23 | * Publishes data in "OpenMetrics" format, e.g. to VictoriaMetrics (https://docs.victoriametrics.com/#how-to-import-data-in-prometheus-exposition-format)
24 | */
25 | @Service
26 | @ConditionalOnProperty(name = "publish.openmetrics.enabled", havingValue = "true")
27 | public class OpenmetricsPushPublisher implements IMeterDataPublisher {
28 |
29 | protected final Logger log = LoggerFactory.getLogger(getClass());
30 |
31 | protected final CloseableHttpClient http = HttpClients.createDefault();
32 |
33 | @Value("${publish.openmetrics.url}")
34 | protected String url;
35 |
36 | @Value("${publish.openmetrics.username}")
37 | protected String username;
38 |
39 | @Value("${publish.openmetrics.password}")
40 | protected String password;
41 |
42 |
43 | @Override
44 | public void publish(SMLMeterData data) throws IOException {
45 | OpenmetricsBuilder builder=new OpenmetricsBuilder();
46 |
47 | for (Reading reading: data.getReadings()) {
48 | if (reading.getName()==null) { // unnamed readings are not supported
49 | continue;
50 | }
51 | builder
52 | .metric(reading.getName())
53 | .tag("meter", data.getMeterId())
54 | .tag("unit", reading.getUnit())
55 | .tag("obis", reading.getObisCode())
56 | .value(reading.getValue());
57 | }
58 |
59 | String payload=builder.build();
60 | if (StringUtils.isBlank(payload)) {
61 | return;
62 | }
63 |
64 | RequestBuilder post = RequestBuilder.post(url);
65 | post.setEntity(new StringEntity(payload, StandardCharsets.UTF_8));
66 | if (!StringUtils.isAnyBlank(username, password)) {
67 | post.addHeader("Authorization","Basic "+Base64.encodeBase64String((username+":"+password).getBytes()));
68 | }
69 |
70 | try (CloseableHttpResponse resp = http.execute(post.build())) {
71 | if (resp.getStatusLine().getStatusCode()<200 || resp.getStatusLine().getStatusCode()>299) {
72 | log.warn("Invalid response from openmetrics push endpoint: {}",resp.getStatusLine());
73 | }
74 | } catch (Exception ex) {
75 | log.warn("Unable to publish data to openmetrics push endpoint",ex);
76 | }
77 |
78 |
79 |
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/sml/ObisNameMap.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.sml;
2 |
3 | import java.util.Map;
4 | import java.util.TreeMap;
5 |
6 | /**
7 | * https://www.bundesnetzagentur.de/DE/Beschlusskammern/BK06/BK6_83_Zug_Mess/835_mitteilungen_datenformate/Mitteilung_24/2_EDIFACT-Konsultationsdokumente/Codeliste%20der%20OBIS-Kennzahlen%20und%20Medien%202.4.pdf?__blob=publicationFile&v=1
8 | * https://www.promotic.eu/en/pmdoc/Subsystems/Comm/PmDrivers/IEC62056_OBIS.htm
9 | * https://www.dzg.de/fileadmin/dzg/content/downloads/produkte-zaehler/dvze/DZG_DVZE_Handbuch_201211.pdf
10 | */
11 | public class ObisNameMap {
12 |
13 | protected static final Map obisNameMap = new TreeMap<>();
14 | static {
15 | obisNameMap.put("1-0:1.8.0*255", "energyImportTotal");
16 | obisNameMap.put("1-0:1.8.1*255", "energyImportTariff1");
17 | obisNameMap.put("1-0:1.8.2*255", "energyImportTariff2");
18 | obisNameMap.put("1-0:2.8.0*255", "energyExportTotal");
19 | obisNameMap.put("1-0:2.8.1*255", "energyExportTariff1");
20 | obisNameMap.put("1-0:2.8.2*255", "energyExportTariff2");
21 | obisNameMap.put("1-0:16.7.0*255", "powerTotal");
22 | obisNameMap.put("1-0:36.7.0*255", "powerL1");
23 | obisNameMap.put("1-0:56.7.0*255", "powerL2");
24 | obisNameMap.put("1-0:76.7.0*255", "powerL3");
25 | obisNameMap.put("1-0:32.7.0*255", "voltageL1");
26 | obisNameMap.put("1-0:52.7.0*255", "voltageL2");
27 | obisNameMap.put("1-0:72.7.0*255", "voltageL3");
28 | obisNameMap.put("1-0:31.7.0*255", "currentL1");
29 | obisNameMap.put("1-0:51.7.0*255", "currentL2");
30 | obisNameMap.put("1-0:71.7.0*255", "currentL3");
31 | obisNameMap.put("1-0:81.7.1*255", "phaseAngleUL2toUL1");
32 | obisNameMap.put("1-0:81.7.2*255", "phaseAngleUL3toUL1");
33 | obisNameMap.put("1-0:81.7.4*255", "phaseAngleIL1toUL1");
34 | obisNameMap.put("1-0:81.7.15*255", "phaseAngleIL2toUL2");
35 | obisNameMap.put("1-0:81.7.26*255", "phaseAngleIL3toUL3");
36 | obisNameMap.put("1-0:14.7.0*255", "networkFrequency");
37 |
38 | }
39 |
40 | public static String get(String obisCode) {
41 | return obisNameMap.get(obisCode);
42 | }
43 |
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/sml/SMLDecoder.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.sml;
2 |
3 | import java.io.ByteArrayInputStream;
4 | import java.io.DataInputStream;
5 | import java.io.IOException;
6 | import java.math.BigDecimal;
7 | import java.math.BigInteger;
8 | import java.text.NumberFormat;
9 | import java.util.ArrayList;
10 |
11 | import org.apache.commons.codec.binary.Hex;
12 | import org.openmuc.jsml.structures.OctetString;
13 | import org.openmuc.jsml.transport.MessageExtractor;
14 | import org.slf4j.Logger;
15 | import org.slf4j.LoggerFactory;
16 |
17 | import de.wyraz.sml.AbstractSMLObject;
18 | import de.wyraz.sml.SMLGetListResponse;
19 | import de.wyraz.sml.SMLListEntry;
20 | import de.wyraz.sml.SMLMessage;
21 | import de.wyraz.sml.SMLMessageParser;
22 | import de.wyraz.sml.SMLMessageParser;
23 | import de.wyraz.sml.SMLPublicCloseResponse;
24 | import de.wyraz.sml.SMLPublicOpenResponse;
25 | import de.wyraz.sml.asn1.ASN1BERTokenizer;
26 | import de.wyraz.tibberpulse.sml.SMLMeterData.Reading;
27 | import de.wyraz.tibberpulse.util.ByteUtil;
28 |
29 | /**
30 | * SML Spec: https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03109/TR-03109-1_Anlage_Feinspezifikation_Drahtgebundene_LMN-Schnittstelle_Teilb.pdf?__blob=publicationFile&v=1
31 | *
32 | * @author mwyraz
33 | *
34 | */
35 | public class SMLDecoder {
36 |
37 | protected static final Logger log = LoggerFactory.getLogger(SMLDecoder.class);
38 |
39 | protected static boolean DUMP_RAW_SML=false;
40 |
41 | public static SMLMeterData decode(byte[] smlPayload) throws IOException {
42 | return decode(smlPayload, true);
43 | }
44 |
45 | public static SMLMeterData decode(byte[] smlPayload, boolean failOnCorruptMessagePart) throws IOException {
46 |
47 | if (log.isDebugEnabled()) {
48 | log.debug("Parsing SML: {}",Hex.encodeHexString(smlPayload));
49 | }
50 |
51 | byte[] messagePayload=extractMessage(smlPayload);
52 |
53 | if (DUMP_RAW_SML) {
54 | System.err.println(ByteUtil.toHex(messagePayload));
55 |
56 | System.err.println("--- Dump start");
57 | new ASN1BERTokenizer(messagePayload).dump(System.err);
58 | System.err.println("--- Dump end");
59 | System.err.println();
60 | }
61 |
62 | SMLMeterData result=new SMLMeterData();
63 |
64 | for (SMLMessage sml: SMLMessageParser.parse(messagePayload)) {
65 | decodeSMLObject(result, sml.getMessageBody());
66 | }
67 |
68 | return result;
69 | }
70 |
71 | protected static void decodeSMLObject(SMLMeterData result,AbstractSMLObject sml) {
72 |
73 | if (sml==null) {
74 | return; // may happen on incomplete SML message
75 | }
76 |
77 | if (sml instanceof SMLPublicCloseResponse) {
78 | // no usable data
79 | return;
80 | }
81 |
82 | if (sml instanceof SMLPublicOpenResponse) {
83 | if (result.meterId==null) {
84 | result.meterId=decodeMeterId(((SMLPublicOpenResponse)sml).getServerId());
85 | }
86 | return;
87 | }
88 |
89 | if (sml instanceof SMLGetListResponse) {
90 | SMLGetListResponse res=(SMLGetListResponse) sml;
91 |
92 | if (result.meterId==null) {
93 | result.meterId=decodeMeterId(res.getServerId());
94 | }
95 | // my meter has "ActSensorTime" as index of seconds - is this somehow useful?
96 | if (res.getValList()!=null) {
97 | for (SMLListEntry e: res.getValList()) {
98 | decodeSMLObject(result, e);
99 | }
100 | }
101 | return;
102 | }
103 |
104 | if (sml instanceof SMLListEntry) {
105 | SMLListEntry e=(SMLListEntry) sml;
106 |
107 | String obisCode=decodeObisCode(e.getObjName());
108 |
109 | if (obisCode==null) {
110 | return; // may happen on incomplete SML message
111 | }
112 |
113 | // System.err.println(obisCode+" "+e.getValue());
114 |
115 | if ("129-129:199.130.3*255".equals(obisCode)) { // manufacturer id
116 | return;
117 | }
118 | if ("1-0:0.0.9*255".equals(obisCode)) { // meter serial
119 | return;
120 | }
121 | if ("1-0:0.0.0*255".equals(obisCode)) { // property number
122 | return;
123 | }
124 | if ("1-0:96.1.0*255".equals(obisCode)) { // meter id
125 | return;
126 | }
127 | if ("1-0:96.5.0*255".equals(obisCode)) { // operation status
128 | return;
129 | }
130 | if ("1-0:96.50.1*1".equals(obisCode)) { // manufacturer id
131 | return;
132 | }
133 | if ("1-0:96.50.1*4".equals(obisCode)) { // manufacturer id
134 | return;
135 | }
136 | if ("1-0:96.50.1*4".equals(obisCode)) { // hardware version
137 | return;
138 | }
139 | if ("1-0:96.50.4*4".equals(obisCode)) { // parameter version
140 | return;
141 | }
142 | if ("1-0:96.90.2*1".equals(obisCode)) { // firmware checksum
143 | return;
144 | }
145 | if ("1-0:0.2.0*0".equals(obisCode)) { // firmware version
146 | return;
147 | }
148 | if ("1-0:97.97.0*0".equals(obisCode)) { // status register
149 | return;
150 | }
151 | if ("129-129:199.130.5*255".equals(obisCode)) { // public key
152 | return;
153 | }
154 |
155 | Reading reading=new Reading();
156 | reading.obisCode=obisCode;
157 | reading.name=ObisNameMap.get(obisCode);
158 | reading.unit=(e.getValUnit()==null)?null:e.getValUnit().toString();
159 | if (e.getValue()!=null) {
160 | reading.value=decodeNumber(e.getValue(),e.getScaler());
161 | }
162 |
163 | if (result.readings==null) {
164 | result.readings=new ArrayList<>();
165 | }
166 | result.readings.add(reading);
167 |
168 | return;
169 | }
170 |
171 | log.warn("SML object not implemented: ",sml.getClass().getName());
172 |
173 | }
174 |
175 | public static Number decodeNumber(Object value, Number scaler) {
176 | if (value==null) {
177 | return null;
178 | }
179 |
180 | int sc=(scaler==null)?0:scaler.intValue();
181 |
182 | if (value instanceof Byte) {
183 | byte val=(Byte)value;
184 | if (sc==0) {
185 | return val;
186 | }
187 | return new BigDecimal(val).scaleByPowerOfTen(sc);
188 | }
189 |
190 | if (value instanceof Short) {
191 | short val=(Short)value;
192 | if (sc==0) {
193 | return val;
194 | }
195 | return new BigDecimal(val).scaleByPowerOfTen(sc);
196 | }
197 |
198 | if (value instanceof Integer) {
199 | int val=(Integer) value;
200 | if (sc==0) {
201 | return val;
202 | }
203 | return new BigDecimal(val).scaleByPowerOfTen(sc);
204 | }
205 |
206 | if (value instanceof Long) {
207 | long val=(Long) value;
208 | if (sc==0) {
209 | return val;
210 | }
211 | return new BigDecimal(val).scaleByPowerOfTen(sc);
212 | }
213 |
214 | if (value instanceof BigInteger) {
215 | BigInteger val=(BigInteger) value;
216 | if (sc==0) {
217 | return val;
218 | }
219 | return new BigDecimal(val).scaleByPowerOfTen(sc);
220 | }
221 |
222 |
223 | log.warn("Number conversion not implemented: {}",value.getClass().getName());
224 | return null;
225 | }
226 |
227 | public static String decodeObisCode(OctetString s) {
228 | return (s==null) ? null : decodeObisCode(s.getValue());
229 | }
230 | public static String decodeObisCode(byte[] bytes) {
231 | StringBuilder sb=new StringBuilder();
232 | sb.append(bytes[0] & 0xff);
233 | sb.append("-");
234 | sb.append(bytes[1] & 0xff);
235 | sb.append(":");
236 | sb.append(bytes[2] & 0xff);
237 | sb.append(".");
238 | sb.append(bytes[3] & 0xff);
239 | sb.append(".");
240 | sb.append(bytes[4] & 0xff);
241 | sb.append("*");
242 | sb.append(bytes[5] & 0xff);
243 | return sb.toString();
244 | }
245 |
246 | /**
247 | * https://netze.estw.de/erlangenGips/Erlangen/__attic__20210120_155237__estw1.de/Kopfnavigation/Netze/Messwesen/Messwesen/Herstelleruebergreifende-Identifikationsnummer-fuer-Messeinrichtungen.pdf
248 | */
249 | public static String decodeMeterId(byte[] bytes) {
250 | StringBuilder sb=new StringBuilder();
251 | // 1st byte is 09 on my meter - no idea what the meaning is
252 | sb.append(Hex.encodeHex(bytes,1,1,false)[1]); // 2nd byte is "media"
253 |
254 | sb.append((char) bytes[2]); // 3 bytes manufacturer
255 | sb.append((char) bytes[3]);
256 | sb.append((char) bytes[4]);
257 |
258 | // following byte is "version" with 2 digits
259 | NumberFormat nf=NumberFormat.getIntegerInstance();
260 | nf.setGroupingUsed(false);
261 | nf.setMinimumIntegerDigits(2);
262 | nf.setMaximumIntegerDigits(2);
263 | sb.append(nf.format(bytes[5]));
264 |
265 | // following 4 bytes are serial with 8 digits
266 |
267 | nf.setMinimumIntegerDigits(8);
268 | nf.setMaximumIntegerDigits(8);
269 | sb.append(nf.format(new BigInteger(bytes, 6, 4)));
270 |
271 | return sb.toString();
272 | }
273 |
274 | protected static byte[] extractMessage(byte[] smlPayload) throws IOException {
275 | try {
276 | return new MessageExtractor(new DataInputStream(new ByteArrayInputStream(smlPayload)),1000).getSmlMessage();
277 | } catch (IOException ex) {
278 | if ("Timeout".equals(ex.getMessage())) {
279 | throw new IOException("Invalid SML payload: "+Hex.encodeHexString(smlPayload));
280 | }
281 | throw new IOException("Invalid SML payload: "+ex.getMessage());
282 | }
283 | }
284 |
285 |
286 | }
287 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/sml/SMLMeterData.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.sml;
2 |
3 | import java.util.List;
4 |
5 | public class SMLMeterData {
6 |
7 | public static class Reading {
8 | protected String obisCode;
9 | protected String name;
10 | protected Number value;
11 | protected String unit;
12 |
13 | public Reading() {
14 | }
15 |
16 | public Reading(String obisCode, String name, Number value, String unit) {
17 | super();
18 | this.obisCode = obisCode;
19 | this.name = name;
20 | this.value = value;
21 | this.unit = unit;
22 | }
23 |
24 | public String getName() {
25 | return name;
26 | }
27 | public String getObisCode() {
28 | return obisCode;
29 | }
30 | public String getUnit() {
31 | return unit;
32 | }
33 | public Number getValue() {
34 | return value;
35 | }
36 |
37 | @Override
38 | public String toString() {
39 | StringBuilder sb=new StringBuilder();
40 | sb.append(obisCode);
41 | if (name!=null) {
42 | sb.append(" / ").append(name);
43 | }
44 | sb.append(" = ").append(value);
45 | if (unit!=null) {
46 | sb.append(" ").append(unit);
47 | }
48 | return sb.toString();
49 | }
50 | }
51 |
52 | public SMLMeterData() {
53 | }
54 |
55 | public SMLMeterData(String meterId, List readings) {
56 | this.meterId=meterId;
57 | this.readings=readings;
58 | }
59 |
60 |
61 | protected String meterId;
62 | protected List readings;
63 |
64 | public String getMeterId() {
65 | return meterId;
66 | }
67 | public List getReadings() {
68 | return readings;
69 | }
70 |
71 | @Override
72 | public String toString() {
73 | StringBuilder sb=new StringBuilder();
74 | sb.append("Meter ").append(meterId);
75 | if (readings!=null) {
76 | for (Reading r: readings) {
77 | sb.append("\n ").append(r);
78 | }
79 | }
80 | return sb.toString();
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/source/MQTTSource.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.source;
2 |
3 | import java.nio.charset.StandardCharsets;
4 | import java.util.UUID;
5 |
6 | import javax.annotation.PostConstruct;
7 |
8 | import org.apache.commons.codec.binary.Hex;
9 | import org.apache.commons.lang3.StringUtils;
10 | import org.eclipse.paho.client.mqttv3.IMqttClient;
11 | import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
12 | import org.eclipse.paho.client.mqttv3.MqttCallback;
13 | import org.eclipse.paho.client.mqttv3.MqttClient;
14 | import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
15 | import org.eclipse.paho.client.mqttv3.MqttMessage;
16 | import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
17 | import org.slf4j.Logger;
18 | import org.slf4j.LoggerFactory;
19 | import org.springframework.beans.factory.annotation.Autowired;
20 | import org.springframework.beans.factory.annotation.Value;
21 |
22 | import de.wyraz.tibberpulse.sink.MeterDataHandler;
23 | import de.wyraz.tibberpulse.sml.SMLDecoder;
24 | import de.wyraz.tibberpulse.sml.SMLMeterData;
25 |
26 | public class MQTTSource {
27 |
28 | protected final Logger log = LoggerFactory.getLogger(getClass());
29 |
30 | @Value("${sml.ignoreCrcErrors:false}")
31 | protected boolean ignoreCrcErrors;
32 |
33 | @Value("${tibber.pulse.mqtt.host}")
34 | protected String mqttHost;
35 |
36 | @Value("${tibber.pulse.mqtt.tls:false}")
37 | protected boolean mqttTls;
38 |
39 | @Value("${tibber.pulse.mqtt.port:1883}")
40 | protected int mqttPort;
41 |
42 | @Value("${tibber.pulse.mqtt.username}")
43 | protected String mqttUsername;
44 |
45 | @Value("${tibber.pulse.mqtt.password}")
46 | protected String mqttPassword;
47 |
48 | @Value("${tibber.pulse.mqtt.topic}")
49 | protected String mqttTopic;
50 |
51 | @Value("${tibber.pulse.mqtt.payloadEncoding}")
52 | protected PayloadEncoding mqttPayloadEncoding;
53 |
54 | protected IMqttClient mqttClient;
55 |
56 | @PostConstruct
57 | public void startMqttClient() throws Exception {
58 |
59 | String protocol=mqttTls?"ssl":"tcp";
60 |
61 | mqttClient = new MqttClient(protocol+"://"+mqttHost+":"+mqttPort,UUID.randomUUID().toString(), new MemoryPersistence());
62 | MqttConnectOptions options = new MqttConnectOptions();
63 | options.setAutomaticReconnect(false);
64 | options.setCleanSession(true);
65 | options.setConnectionTimeout(10);
66 | if (!StringUtils.isAnyBlank(mqttUsername, mqttPassword)) {
67 | options.setUserName(mqttUsername);
68 | options.setPassword(mqttPassword.toCharArray());
69 | }
70 | mqttClient.setCallback(new MqttCallback() {
71 |
72 | @Override
73 | public void messageArrived(String topic, MqttMessage message) throws Exception {
74 | handleMessage(topic, message);
75 | }
76 |
77 | @Override
78 | public void deliveryComplete(IMqttDeliveryToken token) {
79 | }
80 |
81 | @Override
82 | public void connectionLost(Throwable cause) {
83 | System.err.println("Disconnected");
84 | System.exit(0);
85 | }
86 | });
87 |
88 | mqttClient.connect(options);
89 | mqttClient.subscribe(mqttTopic);
90 | }
91 |
92 | @Autowired
93 | protected MeterDataHandler handler;
94 |
95 | protected void handleMessage(String topic, MqttMessage message) {
96 | SMLMeterData data;
97 | try {
98 | byte[] payload=mqttPayloadEncoding.decode(message.getPayload());
99 | if (payload==null) {
100 | return;
101 | }
102 |
103 | data=SMLDecoder.decode(payload, !ignoreCrcErrors);
104 | } catch (Exception ex) {
105 | log.warn("Unable to parse SML from response",ex);
106 | return;
107 | }
108 |
109 | if (data!=null) {
110 | try {
111 | handler.publish(data);
112 | } catch (Exception ex) {
113 | log.warn("Unable publish meter data",ex);
114 | return;
115 | }
116 | }
117 | }
118 |
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/source/PayloadEncoding.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.source;
2 |
3 | import java.nio.charset.StandardCharsets;
4 |
5 | import org.apache.commons.codec.DecoderException;
6 | import org.apache.commons.codec.binary.Hex;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 |
10 | public enum PayloadEncoding {
11 |
12 | HEX {
13 | @Override
14 | byte[] decode(byte[] payload) {
15 | try {
16 | return Hex.decodeHex(new String(payload, StandardCharsets.UTF_8));
17 | } catch (DecoderException ex) {
18 | log.warn("Unable to decode SML as HEX",ex);
19 | return null;
20 | }
21 | }
22 | },
23 | BINARY {
24 | @Override
25 | byte[] decode(byte[] payload) {
26 | return payload;
27 | }
28 | },
29 | ;
30 |
31 | protected static final Logger log = LoggerFactory.getLogger(PayloadEncoding.class);
32 |
33 |
34 | abstract byte[] decode(byte[] payload);
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/source/TibberPulseHttpReader.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.source;
2 |
3 | import java.io.IOException;
4 |
5 | import org.apache.commons.codec.binary.Base64;
6 | import org.apache.commons.lang3.StringUtils;
7 | import org.apache.http.client.methods.CloseableHttpResponse;
8 | import org.apache.http.client.methods.RequestBuilder;
9 | import org.apache.http.impl.client.CloseableHttpClient;
10 | import org.apache.http.impl.client.HttpClients;
11 | import org.apache.http.util.EntityUtils;
12 | import org.slf4j.Logger;
13 | import org.slf4j.LoggerFactory;
14 | import org.springframework.beans.factory.annotation.Autowired;
15 | import org.springframework.beans.factory.annotation.Value;
16 | import org.springframework.boot.CommandLineRunner;
17 | import org.springframework.context.ConfigurableApplicationContext;
18 | import org.springframework.scheduling.annotation.Scheduled;
19 |
20 | import de.wyraz.tibberpulse.sink.MeterDataHandler;
21 | import de.wyraz.tibberpulse.sml.SMLDecoder;
22 | import de.wyraz.tibberpulse.sml.SMLMeterData;
23 |
24 | public class TibberPulseHttpReader implements CommandLineRunner {
25 |
26 | protected final Logger log = LoggerFactory.getLogger(getClass());
27 |
28 |
29 | @Value("${sml.ignoreCrcErrors:false}")
30 | protected boolean ignoreCrcErrors;
31 |
32 | @Value("${tibber.pulse.http.url}")
33 | protected String tibberPulseUrl;
34 |
35 | @Value("${tibber.pulse.http.username}")
36 | protected String tibberPulseUsername;
37 |
38 | @Value("${tibber.pulse.http.password}")
39 | protected String tibberPulsePassword;
40 |
41 | protected final CloseableHttpClient http = HttpClients.createDefault();
42 |
43 | @Autowired
44 | protected ConfigurableApplicationContext ctx;
45 |
46 | @Autowired
47 | protected MeterDataHandler handler;
48 |
49 | @Override
50 | public void run(String... args) throws IOException {
51 | fetch(true);
52 | }
53 |
54 | @Scheduled(cron = "${tibber.pulse.http.cron}")
55 | public void fetch() throws IOException {
56 | fetch(false);
57 | }
58 |
59 | protected void fetch(boolean shutdownOnError) throws IOException {
60 | RequestBuilder get = RequestBuilder.get(tibberPulseUrl);
61 | if (!StringUtils.isAnyBlank(tibberPulseUsername, tibberPulsePassword)) {
62 | get.addHeader("Authorization","Basic "+Base64.encodeBase64String((tibberPulseUsername+":"+tibberPulsePassword).getBytes())) ;
63 | }
64 |
65 | try (CloseableHttpResponse resp = http.execute(get.build())) {
66 | if (resp.getStatusLine().getStatusCode() != 200) {
67 | log.warn("Invalid response from tibber pulse gateway endpoint: {}",resp.getStatusLine());
68 | if (shutdownOnError) {
69 | ctx.close();
70 | }
71 | return;
72 | }
73 | byte[] payload;
74 | try {
75 | payload=EntityUtils.toByteArray(resp.getEntity());
76 |
77 | if (payload.length==0) {
78 | log.debug("Received no data");
79 | return;
80 | }
81 |
82 | } catch (Exception ex) {
83 | log.warn("Unable to extract payload from response",ex);
84 | if (shutdownOnError) {
85 | ctx.close();
86 | }
87 | return;
88 | }
89 |
90 | SMLMeterData data;
91 | try {
92 | data=SMLDecoder.decode(payload, !ignoreCrcErrors);
93 | } catch (Exception ex) {
94 | log.warn("Unable to parse SML from response",ex);
95 | if (shutdownOnError) {
96 | ctx.close();
97 | }
98 | return;
99 | }
100 |
101 | if (data!=null) {
102 | try {
103 | handler.publish(data);
104 | } catch (Exception ex) {
105 | log.warn("Unable publish meter data",ex);
106 | if (shutdownOnError) {
107 | ctx.close();
108 | }
109 | return;
110 | }
111 | }
112 |
113 | } catch (Exception ex) {
114 | if (shutdownOnError) {
115 | log.warn("Unable to fetch data from tibber pulse bridge. Terminating",ex);
116 | ctx.close();
117 | System.exit(1);
118 | } else {
119 | log.warn("Unable to fetch data from tibber pulse bridge",ex);
120 | }
121 | return;
122 | }
123 |
124 | }
125 |
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/source/TibberPulseSourceConfig.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.source;
2 |
3 | import org.springframework.beans.factory.annotation.Value;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 |
7 | @Configuration
8 | public class TibberPulseSourceConfig {
9 |
10 | @Bean
11 | public Object getTibberPulseSource(@Value("${tibber.pulse.source}") String source) {
12 | if ("http".equals(source)) {
13 | return new TibberPulseHttpReader();
14 | }
15 | if ("mqtt".equals(source)) {
16 | return new MQTTSource();
17 | }
18 |
19 | throw new IllegalArgumentException("Source most be 'http' or 'mqtt'");
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/de/wyraz/tibberpulse/util/ByteUtil.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.util;
2 |
3 | public class ByteUtil {
4 |
5 | public static String toBits(byte b) {
6 | StringBuilder sb=new StringBuilder(8);
7 | for (int i=7;i>=0;i--) {
8 | sb.append((b & (1<> 4]);
22 | sb.append(HEX_CHARS[(bytes[i] & 0x0F)]);
23 | }
24 | return sb.toString();
25 | }
26 | public static String int8ToHex(int i) {
27 | return toHex(new byte[] {
28 | (byte) (i & 0xFF)
29 | });
30 | }
31 | public static String int16ToHex(long l) {
32 | return toHex(new byte[] {
33 | (byte) ((l >> 8) & 0xFF),
34 | (byte) (l & 0xFF)
35 | });
36 | }
37 | public static String int24ToHex(long l) {
38 | return toHex(new byte[] {
39 | (byte) ((l >> 16) & 0xFF),
40 | (byte) ((l >> 8) & 0xFF),
41 | (byte) (l & 0xFF)
42 | });
43 | }
44 | public static String int32ToHex(long l) {
45 | return toHex(new byte[] {
46 | (byte) ((l >> 24) & 0xFF),
47 | (byte) ((l >> 16) & 0xFF),
48 | (byte) ((l >> 8) & 0xFF),
49 | (byte) (l & 0xFF)
50 | });
51 | }
52 | public static String int40ToHex(long l) {
53 | return toHex(new byte[] {
54 | (byte) ((l >> 32) & 0xFF),
55 | (byte) ((l >> 24) & 0xFF),
56 | (byte) ((l >> 16) & 0xFF),
57 | (byte) ((l >> 8) & 0xFF),
58 | (byte) (l & 0xFF)
59 | });
60 | }
61 | public static String int64ToHex(long l) {
62 | return toHex(new byte[] {
63 | (byte) ((l >> 56) & 0xFF),
64 | (byte) ((l >> 48) & 0xFF),
65 | (byte) ((l >> 40) & 0xFF),
66 | (byte) ((l >> 32) & 0xFF),
67 | (byte) ((l >> 24) & 0xFF),
68 | (byte) ((l >> 16) & 0xFF),
69 | (byte) ((l >> 8) & 0xFF),
70 | (byte) (l & 0xFF)
71 | });
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/resources/application.yaml:
--------------------------------------------------------------------------------
1 | tibber:
2 | pulse:
3 | source: ${TIBBER_PULSE_SOURCE}
4 | http:
5 | host: ${TIBBER_PULSE_HOST}
6 | nodeId: ${TIBBER_PULSE_NODE_ID:1}
7 | url: ${TIBBER_PULSE_URL:http://${tibber.pulse.http.host}/data.json?node_id=${tibber.pulse.http.nodeId}}
8 | username: ${TIBBER_PULSE_USERNAME:}
9 | password: ${TIBBER_PULSE_PASSWORD:}
10 | cron: ${TIBBER_PULSE_CRON:*/15 * * * * *}
11 | mqtt:
12 | host: "${MQTT_SOURCE_HOST}"
13 | port: "${MQTT_SOURCE_PORT:1883}"
14 | tls: "${MQTT_SOURCE_TLS:false}"
15 | username: "${MQTT_SOURCE_USERNAME:}"
16 | password: "${MQTT_SOURCE_PASSWORD:}"
17 | topic: "${MQTT_SOURCE_TOPIC}"
18 | payloadEncoding: "${MQTT_SOURCE_PAYLOAD_ENCODING:HEX}"
19 |
20 | sml:
21 | ignoreCrcErrors: ${IGNORE_SML_CRC_ERRORS:false}
22 |
23 | logging:
24 | level:
25 | ROOT: warn
26 | de.wyraz.tibberpulse: ${LOG_LEVEL:info}
27 |
28 | publish:
29 | filters: ${PUBLISH_FILTERS:}
30 | interval: "${PUBLISH_INTERVAL:}"
31 | mqtt:
32 | enabled: "${PUBLISH_MQTT_ENABLED:false}"
33 | host: "${PUBLISH_MQTT_HOST}"
34 | port: "${PUBLISH_MQTT_PORT:1883}"
35 | username: "${PUBLISH_MQTT_USERNAME:}"
36 | password: "${PUBLISH_MQTT_PASSWORD:}"
37 | topic: "${PUBLISH_MQTT_TOPIC:{meterId}/{nameOrObisCode}}"
38 | payload: "${PUBLISH_MQTT_VALUE:{value}}"
39 | openmetrics:
40 | enabled: "${PUBLISH_OPENMETRICS_ENABLED:false}"
41 | url: "${PUBLISH_OPENMETRICS_URL}"
42 | username: "${PUBLISH_OPENMETRICS_USERNAME:}"
43 | password: "${PUBLISH_OPENMETRICS_PASSWORD:}"
44 |
--------------------------------------------------------------------------------
/src/test/java/de/wyraz/sml/asn1/ASN1NumberDecoderTest.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.sml.asn1;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import java.math.BigInteger;
6 |
7 | import org.junit.Test;
8 |
9 | public class ASN1NumberDecoderTest {
10 |
11 | protected static Number decodeUnsigned(int... byteArray) {
12 |
13 | byte[] bytes=new byte[byteArray.length];
14 | for (int i=0;i readings=Arrays.asList(
16 | new SMLMeterData.Reading("1-0:1.8.0*255", "energyImportTotal", 123123.45, "WATT_HOUR"),
17 | new SMLMeterData.Reading("1-0:2.8.0*255", "energyExportTotal", 345234.56, "WATT_HOUR"),
18 | new SMLMeterData.Reading("1-0:16.7.0*255", "powerTotal", 11111.22, "WATT"),
19 | new SMLMeterData.Reading("1-0:36.7.0*255", "powerL1", 22333.33, "WATT")
20 | );
21 |
22 | @Test
23 | public void testFilterNone() {
24 | assertThat(new MeterReadingFilter("").apply(readings))
25 | .extracting(Object::toString)
26 | .containsExactlyInAnyOrder(
27 | "1-0:1.8.0*255 / energyImportTotal = 123123.45 WATT_HOUR",
28 | "1-0:2.8.0*255 / energyExportTotal = 345234.56 WATT_HOUR",
29 | "1-0:16.7.0*255 / powerTotal = 11111.22 WATT",
30 | "1-0:36.7.0*255 / powerL1 = 22333.33 WATT"
31 | );
32 | }
33 |
34 | @Test
35 | public void testFilterInvalid() {
36 | assertThatCode(() -> new MeterReadingFilter("somestuff"))
37 | .isInstanceOf(IllegalArgumentException.class)
38 | .hasMessage("Invalid filter spec: somestuff")
39 | ;
40 |
41 | assertThatCode(() -> new MeterReadingFilter("=somestuff"))
42 | .isInstanceOf(IllegalArgumentException.class)
43 | .hasMessage("Invalid filter spec: =somestuff")
44 | ;
45 |
46 | assertThatCode(() -> new MeterReadingFilter("somestuff="))
47 | .isInstanceOf(IllegalArgumentException.class)
48 | .hasMessage("Invalid filter spec: somestuff=")
49 | ;
50 |
51 |
52 | assertThatCode(() -> new MeterReadingFilter("somestuff=UNKNOWN"))
53 | .isInstanceOf(IllegalArgumentException.class)
54 | .hasMessageStartingWith("Invalid filter rule: UNKNOWN. Allowed rules: [IGNORE, KILOWATT(kW)")
55 | ;
56 |
57 | }
58 |
59 | @Test
60 | public void testFilterIgnore() {
61 | assertThat(new MeterReadingFilter("PoWerToTal=IgNoRe 1-0:2.8.0*255=IGNORE").apply(readings))
62 | .extracting(Object::toString)
63 | .containsExactlyInAnyOrder(
64 | "1-0:1.8.0*255 / energyImportTotal = 123123.45 WATT_HOUR",
65 | "1-0:36.7.0*255 / powerL1 = 22333.33 WATT"
66 | );
67 | }
68 |
69 | @Test
70 | public void testFilterKilowatt() {
71 | assertThat(new MeterReadingFilter("powerTotal=KILOWATT").apply(readings))
72 | .extracting(Object::toString)
73 | .containsExactlyInAnyOrder(
74 | "1-0:1.8.0*255 / energyImportTotal = 123123.45 WATT_HOUR",
75 | "1-0:2.8.0*255 / energyExportTotal = 345234.56 WATT_HOUR",
76 | "1-0:16.7.0*255 / powerTotal = 11.11122 KILOWATT",
77 | "1-0:36.7.0*255 / powerL1 = 22333.33 WATT"
78 | );
79 | }
80 |
81 | @Test
82 | public void testFilterKW() {
83 | assertThat(new MeterReadingFilter("powerTotal=kW").apply(readings))
84 | .extracting(Object::toString)
85 | .containsExactlyInAnyOrder(
86 | "1-0:1.8.0*255 / energyImportTotal = 123123.45 WATT_HOUR",
87 | "1-0:2.8.0*255 / energyExportTotal = 345234.56 WATT_HOUR",
88 | "1-0:16.7.0*255 / powerTotal = 11.11122 KILOWATT",
89 | "1-0:36.7.0*255 / powerL1 = 22333.33 WATT"
90 | );
91 | }
92 |
93 | @Test
94 | public void testFilterKWWrongUnit() {
95 | assertThat(new MeterReadingFilter("energyImportTotal=kWh").apply(readings))
96 | .extracting(Object::toString)
97 | .containsExactlyInAnyOrder(
98 | "1-0:1.8.0*255 / energyImportTotal = 123.12345 KILOWATT_HOUR",
99 | "1-0:2.8.0*255 / energyExportTotal = 345234.56 WATT_HOUR",
100 | "1-0:16.7.0*255 / powerTotal = 11111.22 WATT",
101 | "1-0:36.7.0*255 / powerL1 = 22333.33 WATT"
102 | );
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/src/test/java/de/wyraz/tibberpulse/sml/LibSMLTests.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.sml;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 |
5 | import org.apache.commons.codec.binary.Hex;
6 | import org.junit.Ignore;
7 | import org.junit.Test;
8 |
9 | /**
10 | * Tests taken from https://github.com/devZer0/libsml-testing
11 | * @author mwyraz
12 | *
13 | */
14 | public class LibSMLTests {
15 |
16 | static {
17 | SMLDecoder.DUMP_RAW_SML=true;
18 | }
19 |
20 | @Test
21 | public void testDZG_DVS_7412_2_jmberg() throws Exception {
22 | // DZG_DVS-7412.2_jmberg.hex
23 | // Remarks: Wrongly encoded power value 356.24 as -299.12
24 | // FIXME: check what workaround is needed here
25 |
26 | String payload="1B1B1B1B010101017605F12CAD07620062007263010176010102310B0A01445A47000282225E7262016505E748D7620263955C007605F22CAD07620062007263070177010B0A01445A47000282225E070100620AFFFF7262016505E748D77577070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A47000282225E0177070100010800FF641C01047262016200621E52FF65033C93890177070100020800FF017262016200621E52FF650FA49A9E0177070100100700FF017262016200621B52FE538B28010101636B99007605F32CAD076200620072630201710163D90C00001B1B1B1B1A01C3E1";
27 |
28 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
29 |
30 | System.err.println(data);
31 |
32 | assertThat(data).isNotNull();
33 |
34 | assertThat(data.getMeterId()).isEqualTo("1DZG0042082910");
35 |
36 | assertThat(data.getReadings())
37 | .isNotNull()
38 | .extracting(Object::toString)
39 | .containsExactlyInAnyOrder(
40 | "1-0:1.8.0*255 / energyImportTotal = 5430157.7 WATT_HOUR",
41 | "1-0:2.8.0*255 / energyExportTotal = 26244572.6 WATT_HOUR",
42 | "1-0:16.7.0*255 / powerTotal = -299.12 WATT"
43 | );
44 | }
45 |
46 | @Test
47 | public void testDZG_DVS_7420_2V_G2_mtr0() throws Exception {
48 | // DZG_DVS-7420.2V.G2_mtr0.hex
49 |
50 | String payload="1B1B1B1B0101010176057123AF01620062007263010176010102310B0A01445A4700039E2054726201648FCCB4620263379A0076057223AF01620062007263070177010B0A01445A4700039E2054070100620AFFFF726201648FCCB47577070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20540177070100010800FF641C01047262016200621E52FF645CB0670177070100020800FF017262016200621E52FF64C149960177070100100700FF017262016200621B52FE53545F0101016308DD0076057323AF016200620072630201710163E04800001B1B1B1B1A0119671B1B1B1B0101010176057423AF01620062007263010176010102310B0A01445A4700039E2054726201648FCCB5620263EA830076057523AF";
51 |
52 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
53 |
54 | System.err.println(data);
55 |
56 | assertThat(data).isNotNull();
57 |
58 | assertThat(data.getMeterId()).isEqualTo("1DZG0060694612");
59 |
60 | assertThat(data.getReadings())
61 | .isNotNull()
62 | .extracting(Object::toString)
63 | .containsExactlyInAnyOrder(
64 | "1-0:1.8.0*255 / energyImportTotal = 607447.1 WATT_HOUR",
65 | "1-0:2.8.0*255 / energyExportTotal = 1266728.6 WATT_HOUR",
66 | "1-0:16.7.0*255 / powerTotal = 215.99 WATT"
67 | );
68 | }
69 |
70 | @Test
71 | public void testDZG_DVS_7420_2V_G2_mtr1() throws Exception {
72 | // DZG_DVS-7420.2V.G2_mtr1.hex
73 |
74 | String payload="1B1B1B1B010101017605D225AF01620062007263010176010102310B0A01445A4700039E2055726201648FCD5C6202630172007605D325AF01620062007263070177010B0A01445A4700039E2055070100620AFFFF726201648FCD5C7777070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20550177070100010800FF641C00047262016200621E52FF65016723B90177070100010801FF017262016200621E52FF6405E4340177070100010802FF017262016200621E52FF6501613F850177070100020800FF017262016200621E52FF62000177070100100700FF017262016200621B52FE53026E01010163BA5B007605D425AF016200620072630201710163E806000000001B1B1B1B1A0311641B1B1B1B010101017605D525AF01620062007263010176010102310B0A01445A4700039E2055726201648FCD5D6202637317007605D625AF01620062007263070177010B0A01445A4700039E2055070100620AFFFF726201648FCD5D7777070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20550177070100010800FF641C00047262016200621E52FF65016723B90177070100010801FF017262016200621E52FF6405E4340177070100010802FF017262016200621E52FF6501613F850177070100020800FF017262016200621E52FF62000177070100100700FF017262016200621B52FE530254010101635543007605D725AF016200620072630201710163D685000000001B1B1B1B1A0358AF1B1B1B1B010101017605D825AF01620062007263010176010102310B0A01445A4700039E2055726201648FCD5E620263BB41007605D925AF01620062007263070177010B0A01445A4700039E2055070100620AFFFF726201648FCD5E7777070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20550177070100010800FF641C00047262016200621E52FF65016723B90177070100010801FF017262016200621E52FF6405E4340177070100010802FF017262016200621E52FF6501613F850177070100020800FF017262016200621E52FF62000177070100100700FF017262016200621B52FE53026D010101630067007605DA25AF016200620072630201710163E6E7000000001B1B1B1B1A0314151B1B1B1B010101017605DB25AF01620062007263010176010102310B0A01445A4700039E2055726201648FCD5F62026397DD007605DC25AF01620062007263070177010B0A01445A4700039E2055070100620AFFFF726201648FCD5F7777070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20550177070100010800FF641C00047262016200621E52FF65016723B90177070100010801FF017262016200621E52FF6405E4340177070100010802FF017262016200621E52FF6501613F850177070100020800FF017262016200621E52FF62000177070100100700FF017262016200621B52FE53024301010163A2CE007605DD25AF0162006200726302017101636197000000001B1B1B1B1A035AC11B1B1B1B010101017605DE25AF01620062007263010176010102310B0A01445A4700039E2055726201648FCD60620263FF52007605DF25AF01620062007263070177010B0A01445A4700039E205507010062";
75 |
76 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
77 |
78 | System.err.println(data);
79 |
80 | assertThat(data).isNotNull();
81 |
82 | assertThat(data.getMeterId()).isEqualTo("1DZG0060694613");
83 |
84 | assertThat(data.getReadings())
85 | .isNotNull()
86 | .extracting(Object::toString)
87 | .containsExactlyInAnyOrder(
88 | "1-0:1.8.0*255 / energyImportTotal = 2353656.9 WATT_HOUR",
89 | "1-0:1.8.1*255 / energyImportTariff1 = 38610.0 WATT_HOUR",
90 | "1-0:1.8.2*255 / energyImportTariff2 = 2315046.9 WATT_HOUR",
91 | "1-0:2.8.0*255 / energyExportTotal = 0.0 WATT_HOUR",
92 | "1-0:16.7.0*255 / powerTotal = 6.22 WATT"
93 | );
94 | }
95 |
96 | @Test
97 | public void testDZG_DVS_7420_2V_G2_mtr2() throws Exception {
98 | // DZG_DVS-7420.2V.G2_mtr2.hex
99 |
100 | String payload="1B1B1B1B010101017605CA24AF01620062007263010176010102310B0A01445A4700039E2053726201648FCC6F6202630B44007605CB24AF01620062007263070177010B0A01445A4700039E2053070100620AFFFF726201648FCC6F7577070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20530177070100010800FF641C00047262016200621E52FF640200E80177070100020800FF017262016200621E52FF64E4D65D0177070100100700FF017262016200621B52FE5302A501010163720B007605CC24AF016200620072630201710163F13A00001B1B1B1B1A01AAE41B1B1B1B010101017605CD24AF01620062007263010176010102310B0A01445A4700039E2053726201648FCC70620263F7B4007605CE24AF01620062007263070177010B0A01445A4700039E2053070100620AFFFF726201648FCC707577070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20530177070100010800FF641C00047262016200621E52FF640200E80177070100020800FF017262016200621E52FF64E4D65D0177070100100700FF017262016200621B52FE5302900101016330C3007605CF24AF016200620072630201710163CFB900001B1B1B1B1A013DD31B1B1B1B010101017605D024AF01620062007263010176010102310B0A01445A4700039E2053726201648FCC71620263CCAA007605D124AF01620062007263070177010B0A01445A4700039E2053070100620AFFFF726201648FCC717577070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20530177070100010800FF641C00047262016200621E52FF640200E80177070100020800FF017262016200621E52FF64E4D65D0177070100100700FF017262016200621B52FE53029C01010163E184007605D224AF016200620072630201710163280D00001B1B1B1B1A01E334";
101 |
102 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
103 |
104 | System.err.println(data);
105 |
106 | assertThat(data).isNotNull();
107 |
108 | assertThat(data.getMeterId()).isEqualTo("1DZG0060694611");
109 |
110 | assertThat(data.getReadings())
111 | .isNotNull()
112 | .extracting(Object::toString)
113 | .containsExactlyInAnyOrder(
114 | "1-0:1.8.0*255 / energyImportTotal = 13130.4 WATT_HOUR",
115 | "1-0:2.8.0*255 / energyExportTotal = 1499708.5 WATT_HOUR",
116 | "1-0:16.7.0*255 / powerTotal = 6.77 WATT"
117 | );
118 | }
119 |
120 | @Test
121 | public void testDZG_DVS_7420_2V_G2_mtr2_neg() throws Exception {
122 | // DZG_DVS-7420.2V.G2_mtr2_neg.hex
123 | // Remarks: Generator, has 2-bytes negative value 16.7.0
124 |
125 | String payload="1B1B1B1B0101010176051338B301620062007263010176010102310B0A01445A4700039E2053726201649128386202632F2B0076051438B301620062007263070177010B0A01445A4700039E2053070100620AFFFF726201649128387577070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20530177070100010800FF641C69047262016200621E52FF640204E90177070100020800FF017262016200621E52FF64E4EE4D0177070100100700FF017262016200621B52FE53D6CA01010163A8900076051538B301620062007263020171016350C300001B1B1B1B1A010DE31B1B1B1B0101010176051638B301620062007263010176010102310B0A01445A4700039E205372620164912839620263F2320076051738B301620062007263070177010B0A01445A4700039E2053070100620AFFFF726201649128397577070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20530177070100010800FF641C69047262016200621E52FF640204E90177070100020800FF017262016200621E52FF64E4EE4E0177070100100700FF017262016200621B52FE53D64A010101636A9B0076051838B301620062007263020171016360A100001B1B1B1B1A0156B71B1B1B1B0101010176051938B301620062007263010176010102310B0A01445A4700039E20537262016491283A62026395180076051A38B301620062007263070177010B0A01445A4700039E2053070100620AFFFF7262016491283A7577070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20530177070100010800FF641C69047262016200621E52FF640204E90177070100020800FF017262016200621E52FF64E4EE4E0177070100100700FF017262016200621B52FE53D73A0101016371820076051B38B30162006200726302017101635E2200001B1B1B1B1A014BA01B1B1B1B0101010176051C38B301620062007263010176010102310B0A01445A4700039E20537262016491283B62026348010076051D38B301620062007263070177010B0A01445A4700039E2053070100620AFFFF7262016491283B7577070100603201010172620162006200520004445A470177070100600100FF017262016200620052000B0A01445A4700039E20530177070100010800FF641C69047262016200621E52FF640204E90177070100020800FF017262016200621E52FF64E4EE4E017707010010";
126 |
127 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
128 |
129 | System.err.println(data);
130 |
131 | assertThat(data).isNotNull();
132 |
133 | assertThat(data.getMeterId()).isEqualTo("1DZG0060694611");
134 |
135 | assertThat(data.getReadings())
136 | .isNotNull()
137 | .extracting(Object::toString)
138 | .containsExactlyInAnyOrder(
139 | "1-0:1.8.0*255 / energyImportTotal = 13232.9 WATT_HOUR",
140 | "1-0:2.8.0*255 / energyExportTotal = 1500321.3 WATT_HOUR",
141 | "1-0:16.7.0*255 / powerTotal = -105.50 WATT"
142 | );
143 | }
144 |
145 | @Test
146 | public void testDrNeuhaus_SMARTY_ix_130() throws Exception {
147 | // DrNeuhaus_SMARTY_ix-130.hex
148 |
149 | String payload="1B1B1B1B010101017605000004FE620062007263010176010105AA0100000B0901444E540100002030010163A802007605000004FF620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52A97777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D84501010163330A007605000005006200620072630201710163389800001B1B1B1B1A01C5741B1B1B1B01010101760500000501620062007263010176010105AB0100000B0901444E5401000020300101632D9F00760500000502620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52AB7777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D84501010163B6DE0076050000050362006200726302017101638B6600001B1B1B1B1A01DA891B1B1B1B01010101760500000504620062007263010176010105AC0100000B0901444E5401000020300101633F4C00760500000505620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52AC7777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D8450101016305380076050000050662006200726302017101634F6D00001B1B1B1B1A013DC41B1B1B1B01010101760500000507620062007263010176010105AD0100000B0901444E540100002030010163253D00760500000508620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52AD7777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D845010101631D15007605000005096200620072630201710163037100001B1B1B1B1A01F4941B1B1B1B0101010176050000050A620062007263010176010105AE0100000B0901444E54010000203001016382A10076050000050B620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52AF7777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D84501010163054D0076050000050C6200620072630201710163C77A00001B1B1B1B1A01C3171B1B1B1B0101010176050000050D620062007263010176010105AF0100000B0901444E54010000203001016354530076050000050E620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52B07777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D845010101638E320076050000050F6200620072630201710163748400001B1B1B1B1A01807F1B1B1B1B01010101760500000510620062007263010176010105B00100000B0901444E540100002030010163441800760500000511620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52B17777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D8450101016363890076050000051262006200726302017101635F4200001B1B1B1B1A01063F1B1B1B1B01010101760500000513620062007263010176010105B10100000B0901444E5401000020300101635E6900760500000514620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52B37777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D845010101638A3D007605000005156200620072630201710163B9E200001B1B1B1B1A0129BC1B1B1B1B01010101760500000516620062007263010176010105B20100000B0901444E54010000203001016370FA00760500000517620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52B47777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D84501010163676C007605000005186200620072630201710163D75500001B1B1B1B1A01E5CE1B1B1B1B01010101760500000519620062007263010176010105B30100000B0901444E5401000020300101632F070076050000051A620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52B57777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D845010101637F410076050000051B620062007263020171016364AB00001B1B1B1B1A017E821B1B1B1B0101010176050000051C620062007263010176010105B40100000B0901444E5401000020300101633DD40076050000051D620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52B77777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D8450101016339AE0076050000051E6200620072630201710163A0A000001B1B1B1B1A01FE291B1B1B1B0101010176050000051F620062007263010176010105B50100000B0901444E54010000203001016327A500760500000520620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52B87777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF59000000000000000001770701000F0700FF0101621B520065000000000177078181C78205FF010101018302FEA5D6BC658D734673F5790051D61C2EBD55344A6285686DE3B645EE0104D65F5ABBA6CB68F1BFCB7FCF94F99CD4D84501010163F593007605000005216200620072630201710163232F00001B1B1B1B1A0168041B1B1B1B01010101760500000522620062007263010176010105B60100000B0901444E540100002030010163B61800760500000523620062007263070177010B0901444E540100002030070100620AFFFF72620165715D52B97777078181C78203FF0101010104444E540177070100000009FF010101010B0901444E5401000020300177070100020800FF6401010001621E52FF59000000000000AAD20177070100020801FF0101621E52FF59000000000000AAD20177070100020802FF0101621E52FF590000000000000000017707";
150 |
151 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
152 |
153 | System.err.println(data);
154 |
155 | assertThat(data).isNotNull();
156 |
157 | assertThat(data.getMeterId()).isEqualTo("1DNT0100008240");
158 |
159 | assertThat(data.getReadings())
160 | .isNotNull()
161 | .extracting(Object::toString)
162 | .containsExactlyInAnyOrder(
163 | "1-0:2.8.0*255 / energyExportTotal = 4373.0 WATT_HOUR",
164 | "1-0:2.8.1*255 / energyExportTariff1 = 4373.0 WATT_HOUR",
165 | "1-0:2.8.2*255 / energyExportTariff2 = 0.0 WATT_HOUR",
166 | "1-0:15.7.0*255 = 0 WATT"
167 | );
168 | }
169 |
170 | @Test
171 | @Ignore // workaround not yet in place
172 | public void testEMH_ED300L_consumption() throws Exception {
173 | // EMH-ED300L_consumption.hex
174 |
175 |
176 | String payload="1B01760004C96272017601000FED45480256450199071652EF00006301064D0171CD01010F767707818203010145487701000001010B45480256CD070008FF01011E0047F977010101011EFF0047F97707010201625256000077010F00016252000301078182FF01010230EFA3DC46052FB9CBA3959C5016B286B13C194298DBA0016322760016F20000630101F7001B1B01851B1B0101071652F46200630101071665ED45480256450144760004C96262726301064D0171CD01010F76770781820301044D0107000001010B45480256CD070008FF01011E560A200107000801011EFF0047FA7707010201625256000077010F0001625200AB7781C7050183E85118880B0527CB80B2FD7A1674D0DB57BE38429806010161000716F800006301017F001B1B01851B0101760004C96200630101071665ED064D0171CD0163EE071652FB000063770B4548025645016565A07781C70301014548770100000101064D0171CD770101006382621E0047FB77010101011EFF000A200107010201625256000077010F0001625200C67781C705010101018302E830EFA3DC46052FB9CBA3959C5016B286B13C194298060101D7000716FE000063010108001B1B01F81B1B0101071652006272017601000FEE4548025645012B760004CA62627263770B454802564501010F767707818203FF0101010104454D480177070100000009FF010101010B06454D4801027156CD070008FF01011E0047FC770101016252560A20010708FF011EFF000001070007FF011B52000301078182FF0183E85118880B052FB9CBA3959C501674D0DB57BE384298DBA0016301760016CA626272027163BF1B1B1AC61B0101071652066272017601000FEE454802564501A2760004CA6262726301064D0171CD016565A77781C703010145487701000001010B45480256CD070008FF01011E0047FD770101016252560A20010708FF015256000077010F000162527781C7050183E85118880B0527CB80B2FD7A1662001B1B014C1B1B01010716520C6272017601000FEE454802564501280716520D00006301064D56010F76770781820301454877010000010106480256CD070008FF01011E0047FE770101016252FF0047FE770708FF011EFF000077010F0001625200C87781C7050183E85118880B0527CB80B2FD7A16B286B13C19429806010181000716100000630101AF1B1B1A9F1B1B01010716521262720101071665EE454802564501D50704CA6262726301064D0171CD010F76770781820301044D010700000101064D0171CD070008FF01011E016252560A20010708FF011EFF0000070007FF011B5200BD7781C7050183E85118880B0527CB80B2FD7A16B286B13C194298060101F9000716160000630101D81B1B1AA41B1B0101760004CA6200630101071665EE064D0171CD01633C0716521900006301064802564501010F76770781820301044D010700000101064D0171CD070008FF01011E004700770101016252560A210107010201625256000001070007FF011B52000301078182FF0183E851A3DC460527CB80B2FD7A16B286B13C1942980601014D7600161C000002716350001B1B01EF1B0101760004CA62630101071665EE454802564501B50716521F00006301064D0171CD010F76770781820301044D010700000101064D0171CD070008FF01011E004701770101016252560A21010708FF011EFF000077010F0001625200797781C7050183E85118880B0527CB80B2FD7A16B286B13C194298A0016339760016220000630101421B1B1A861B0101760004CA6200630101071665EE4D0171564501C60004CA62627263770B45480256450181820301044D010700000101064D0171CD070008FF01011E004702770101016252560A21010708FF011EFF000077010F0001625200A77781C7FF0183E85118880B05B9CBA3959C5016B28657BE384298060101EF000716280000630101CA1B1B1A271B1B0101760004CA6272017601000FEE4548025645016C0716526262726301064D0171CD016565BE7781C70301044D010700000101064D0171CD450177070100010800FF63018201621E004703770101016252560A210107010201625256000001070007FF011B5200A87781C7050183E85118880B0527CB80B2FD7A1674D0DB57BE384298DBA00163A37600162E0000630101BD001B1B01271B1B01010716526272017601000FEE454802564501050716523100006301064D0171CD016565C27781C70301044D010700000101064D0171CD070008FF01011E004704770101016252560A210107010201625256000077010F0001625200B67781C705010101018302E83051EF18A388DC0B4605B9CBA3959C5016B286B13C194298060101A0000716CA626272027163701B1B1A1D1B1B0101071652366272017601000FEE4548025645018C760004CA6262726301064D0171CD016565C67781C70301044D010700000101064D0171CD070008FF01011E004705770101016252560A21010708FF011EFF000077010F0001625200E67781C7050183E85118880B0527CB80B2FD7A1674D0DB57BE384298DBA00163917600163A0000630101AD1B1B1A631B01010716523C6272017601000FEE45480256450106760004CA6262726301064D0171CD01010F76770781820301044D010700000101064D0171CD770101006382621E004706770101016252560A21770708FF011EFF000077010F0001625200C77781C7FF0183E85118880B0527CB80B2FD7A16B286B13C194298060101C3000716400000630101EF1B1B1A7E1B0101071652426272017601000FEE454802564501180716524300006301064D0171CD016565CD7781C70301044D0107000001010B45480256CD0701006382621E560A2101070008016252560A21010708FF011EFF000077010F0001625200C87781C7050183E85118880B052FCB80B2FD7A1674D0DB57BE384298060163AF760016460000630101981B1B1AE21B01017600046272017601000FEE4D0171CD0163F10716524900006301064D0171CD016565D07781C70301044D0107000001010B45480256CD070008FF01011E52FF56000A4721080177070100010801011EFF004701070201625256000077010F00016252000501078182FF01010230EFA3DC460527CB80B2FD7A16B2DB57BE384298060101FF000716CA626272027163101B1B1A6E1B0101760004CA6272017601000FEE454802564501780716524F00006301064D0171CD016565D17781C70301044D010700000101064D0171CD070008FF01011E00210107000801011EFF00470B770708FF011EFF000077010F000162520001078182FF0183E85118880B052FB9CBA3959C5016B286B13C19429806010121000716520000630101881B1B01F61B1B0101760004CA6272017601000FEE454802564501A50716525500006301064D0171CD016565D47781C70301044D01070000010B45480256CD070008FF01011E002101070008016252560A21010708FF011EFF000077010F000162520001078182FF01010230EFA3DC460527CB80B2FD7A16B286B13C19429806010166000716580000630101001B1B1A0C1B1B01760004CA62720101071665EE4548025645010F0004CA6262726301064D0171CD016565D777818203014548770100000101064D0171CD070008FF01011E00470E770101016252560A21010702016252560000000177010F07FF01520501078182FF0183E85118880B0527CB80B2FD7A16B286B13C1942980601014B0007165E0000630101771B1B1A211B1B0101071652606200630101000FEE4D0171CD0163770004CA6262726301064D0171CD016565D97781C70301044D01070000010B45480256CD070008FF01011E00470107000801625200470F770708FF011EFF000077010F00016252002A7781C7050183E85118880B05B9CBA3959C501674D0DB57BE38429806010128000716640000630101301B1B1A291B0101760004CA6272017601000FEE454802564501FE0716526700006301064D0171CD016565DC7781C703014548770100000101064D0171CD070008FF01011E004710770101016252560A21010708FF015256000077010F00016252006C7781C7050183E85118880B0527CB80B2FD7A16B286B13C194298060101A00007166A0000630101ED1B1B1AF81B0101760004CA62720101071665EE454802564501740004CA6262726301064D0171CD01650703014548770100000101064D0171CD070008FF01011E004711770101016252564711770708FF011EFF000077010F0001625200497781C7050183E85118880B0527CB80B2FD7A16B286B13C19429806010147000716700000630101201B1B1A2D1B0101071652726272017601000FEE454802564501890716527300006301064D0171CD016565E17781C70301044D01070000010B45480256CD070008FF01011E00471277010101FF0101621E52FF56000A4721120177070102016252560077010F00016252000501078182FF0183E85118880B0527CB80B2FD7A16B286B13C194298060101F400071676000063010157001B1B1A221B1B1B1B01010101760700160452CA7862006200726301017601010700160F65EE280B06454D4801027156CD4501016360F500760700160452CA79620062007263070177010B06454D4801027156CD4501726201650F6576E37777078181C78203FF0101010104454D480177070100000009FF010101010B06454D4801027156CD450177070100010800FF63018201621E52FF56000A4721130177070100010801FF0101621E52FF56000A4721130177070100010802FF0101621E52FF56000000000001770701000F0700FF0101621B52FF550000055B0177078181C78205FF010101018302E83051EF18A388DC0B4605DBFF2F27B9CBCB80A3B295FD9C7A5016D42774B2D086DBB1573CBE193842429840EDDB06A0010101634C0000760700160452CA7C6200620072630201710163DF8800001B1B1B1B1A01B01F1B1B1B1B01010101760700160452CA7E62006200726301017601010700160F65EE2A0B06454D4801027156CD45010163E93300760700160452CA7F620062007263070177010B06454D4801027156CD4501726201650F6576E67777078181C78203FF0101010104454D480177070100000009FF010101010B06454D4801027156CD450177070100010800FF63018201621E52FF56000A4721140177070100010801FF016252560A210107010201625256000001070007FF011B525500AB7781C70501010230EFA3DC46052FB9CBA3959C501674D0DB57BE38980601014C000716820000630101D31B1B1AC51B0101760004CA6272017601000FEE064D0171CD01634D0716528500006301064D0171CD016565E87781C70301014548770100000101064D0171CD070008FF01011E560A2101070008016252560A21010708FF62525600000077010007FF016252000501078182FF01010230EFA3DC46052FB9CBA3959C5016B286B13C194298060101A90007168800006301015B1A001B1B01731B1B0101760004CA6200630101071665EE064D0171CD0163E7000716528B000063770B4548025645016565EB7781C70301014548770100000101064D0171CD0700006382621E560A2101070008016252560A21010708FF011EFF000001070007FF011B52000501078182FF0183E85118880B052F";
177 |
178 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
179 |
180 | System.err.println(data);
181 |
182 | assertThat(data).isNotNull();
183 |
184 | assertThat(data.getMeterId()).isEqualTo("1DNT0100008240");
185 |
186 | assertThat(data.getReadings())
187 | .isNotNull()
188 | .extracting(Object::toString)
189 | .containsExactlyInAnyOrder(
190 | "1-0:2.8.0*255 / energyExportTotal = 4373.0 WATT_HOUR",
191 | "1-0:2.8.1*255 / energyExportTariff1 = 4373.0 WATT_HOUR",
192 | "1-0:2.8.2*255 / energyExportTariff2 = 0.0 WATT_HOUR",
193 | "1-0:15.7.0*255 = 0 WATT"
194 | );
195 | }
196 |
197 |
198 | }
199 |
--------------------------------------------------------------------------------
/src/test/java/de/wyraz/tibberpulse/sml/SMLDecoderTests.java:
--------------------------------------------------------------------------------
1 | package de.wyraz.tibberpulse.sml;
2 |
3 | import static org.assertj.core.api.Assertions.assertThat;
4 | import static org.assertj.core.api.Assertions.assertThatCode;
5 |
6 | import java.io.IOException;
7 |
8 | import org.apache.commons.codec.binary.Hex;
9 | import org.junit.Test;
10 |
11 | import de.wyraz.sml.asn1.ASN1BERTokenizer;
12 |
13 | public class SMLDecoderTests {
14 |
15 | static {
16 | SMLDecoder.DUMP_RAW_SML=true;
17 | }
18 |
19 | @Test
20 | public void testSMLDecoderInvalidStartSequence() throws Exception {
21 | String payload="1c1b1b1b010101017605128e14cf620062007265000001017601010765425a4444330b090145425a0100091f14010163df26007605128e14d06200620072650000070177010b090145425a0100091f14017262016505e465007a77078181c78203ff010101010445425a0177070100000009ff010101010b090145425a0100091f140177070100010800ff6401018001621e52fb69000002a05939af070177070100010801ff0101621e52fb69000002a0531f2f070177070100010802ff0101621e52fb6900000000061a80000177070100020800ff6401018001621e52fb69000000000d7b8b460177070100100700ff0101621b52fe5500013c060177070100240700ff0101621b52fe55000058f00177070100380700ff0101621b52fe55000085f901770701004c0700ff0101621b52fe5500005d1d010101636c93007605128e14d162006200726500000201710163c682000000001b1b1b1b1a032f09";
22 |
23 | assertThatCode(()->{
24 | SMLDecoder.decode(Hex.decodeHex(payload));
25 | })
26 | .isInstanceOf(IOException.class)
27 | .hasMessageStartingWith("Invalid SML payload: 1c1b1b1");
28 | }
29 | @Test
30 | public void testSMLDecoderInvalidEndSequence() throws Exception {
31 | String payload="1b1b1b1b010101017605128e14cf620062007265000001017601010765425a4444330b090145425a0100091f14010163df26007605128e14d06200620072650000070177010b090145425a0100091f14017262016505e465007a77078181c78203ff010101010445425a0177070100000009ff010101010b090145425a0100091f140177070100010800ff6401018001621e52fb69000002a05939af070177070100010801ff0101621e52fb69000002a0531f2f070177070100010802ff0101621e52fb6900000000061a80000177070100020800ff6401018001621e52fb69000000000d7b8b460177070100100700ff0101621b52fe5500013c060177070100240700ff0101621b52fe55000058f00177070100380700ff0101621b52fe55000085f901770701004c0700ff0101621b52fe5500005d1d010101636c93007605128e14d162006200726500000201710163c682000000001c1b1b1b1a032f09";
32 |
33 | assertThatCode(()->{
34 | SMLDecoder.decode(Hex.decodeHex(payload));
35 | })
36 | .isInstanceOf(IOException.class)
37 | .hasMessageStartingWith("Invalid SML payload: 1b1b1b1");
38 | }
39 | @Test
40 | public void testSMLDecoderInvalidCRC() throws Exception {
41 | String payload="1b1b1b1b010101017605128e14cf620062007265000001017601010765425a4444330b090145425a0100091f14010163df26007605128e14d06200620072650000070177010b090145425a0100091f14017262016505e465007a77078181c78203ff010101010445425a0177070100000009ff010101010b090145425a0100091f140177070100010800ff6401018001621e52fb69000002a05939af070177070100010801ff0101621e52fb69000002a0531f2f070177070100010802ff0101621e52fb6900000000061a80000177070100020800ff6401018001621e52fb69000000000d7b8b460177070100100700ff0101621b52fe5500013c060177070100240700ff0101621b52fe55000058f00177070100380700ff0101621b52fe55000085f901770701004c0700ff0101621b52fe5500005d1d010101636c93007605128e14d162006200726500000201710163c682000000001b1b1b1b1a032f08";
42 |
43 | assertThatCode(()->{
44 | SMLDecoder.decode(Hex.decodeHex(payload));
45 | })
46 | .isInstanceOf(IOException.class)
47 | .hasMessage("Invalid SML payload: wrong crc");
48 | }
49 |
50 | /**
51 | * Initial test with data from my own meter
52 | */
53 | @Test
54 | public void testSMLDecoderEBZ() throws Exception {
55 | String payload="1b1b1b1b010101017605128e14cf620062007265000001017601010765425a4444330b090145425a0100091f14010163df26007605128e14d06200620072650000070177010b090145425a0100091f14017262016505e465007a77078181c78203ff010101010445425a0177070100000009ff010101010b090145425a0100091f140177070100010800ff6401018001621e52fb69000002a05939af070177070100010801ff0101621e52fb69000002a0531f2f070177070100010802ff0101621e52fb6900000000061a80000177070100020800ff6401018001621e52fb69000000000d7b8b460177070100100700ff0101621b52fe5500013c060177070100240700ff0101621b52fe55000058f00177070100380700ff0101621b52fe55000085f901770701004c0700ff0101621b52fe5500005d1d010101636c93007605128e14d162006200726500000201710163c682000000001b1b1b1b1a032f09";
56 |
57 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload));
58 |
59 | assertThat(data).isNotNull();
60 |
61 | assertThat(data.getMeterId()).isEqualTo("1EBZ0100597780");
62 |
63 | assertThat(data.getReadings())
64 | .isNotNull()
65 | .extracting(Object::toString)
66 | .containsExactlyInAnyOrder(
67 | "1-0:1.8.0*255 / energyImportTotal = 28877149.75495 WATT_HOUR",
68 | "1-0:1.8.1*255 / energyImportTariff1 = 28876125.75495 WATT_HOUR",
69 | "1-0:1.8.2*255 / energyImportTariff2 = 1024.00000 WATT_HOUR",
70 | "1-0:2.8.0*255 / energyExportTotal = 2262.00390 WATT_HOUR",
71 | "1-0:16.7.0*255 / powerTotal = 809.02 WATT",
72 | "1-0:36.7.0*255 / powerL1 = 227.68 WATT",
73 | "1-0:56.7.0*255 / powerL2 = 342.97 WATT",
74 | "1-0:76.7.0*255 / powerL3 = 238.37 WATT"
75 | );
76 |
77 | }
78 |
79 | @Test
80 | public void testSMLDecoderEFR_short() throws Exception {
81 | String payload="1b1b1b1b010101017605080e16b66200620072630101760107ffffffffffff0502af5ce80b0a014546522102cd630c7262016502af5ce5016333fe007605080e16b762006200726307017707ffffffffffff0b0a014546522102cd630c070100620affff7262016502af5ce579770701006032010101010101044546520177070100600100ff010101010b0a014546522102cd630c0177070100010800ff641c40047262016502af5ce5621e52ff65016d58b80177070100020800ff017262016502af5ce5621e52ff650286c57c017707010000020000010101010630332e30300177070100605a0201010101010342bd01770701006161000001010101030000017707010060320104010101010850312e322e31320177070100603204040101010103042201010163c14a007605080e16b862006200726302017101639568000000001b1b1b1b1a0309d3";
82 |
83 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload));
84 |
85 | System.err.println(data);
86 |
87 | assertThat(data).isNotNull();
88 |
89 | assertThat(data.getMeterId()).isEqualTo("1EFR3347014668");
90 |
91 | assertThat(data.getReadings())
92 | .isNotNull()
93 | .extracting(Object::toString)
94 | .containsExactlyInAnyOrder(
95 | "1-0:1.8.0*255 / energyImportTotal = 2394335.2 WATT_HOUR",
96 | "1-0:2.8.0*255 / energyExportTotal = 4238681.2 WATT_HOUR"
97 | );
98 |
99 | }
100 |
101 | @Test
102 | public void testSMLDecoderEFR_long() throws Exception {
103 | String payload="1b1b1b1b010101017605080e4fb36200620072630101760107ffffffffffff0502af6fe70b0a014546522102cd630c7262016502af6fe40163ca28007605080e4fb462006200726307017707ffffffffffff0b0a014546522102cd630c070100620affff7262016502af6fe4f106770701006032010101010101044546520177070100600100ff010101010b0a014546522102cd630c0177070100010800ff641c58047262016502af6fe4621e52ff65016d59500177070100020800ff017262016502af6fe4621e52ff650286c63b0177070100100700ff0101621b520052f70177070100200700ff0101622352ff6309310177070100340700ff0101622352ff63093a0177070100480700ff0101622352ff63093701770701001f0700ff0101622152fe62680177070100330700ff0101622152fe62b80177070100470700ff0101622152fe62740177070100510701ff01016208520052790177070100510702ff0101620852005300ee0177070100510704ff010162085200530108017707010051070fff010162085200530120017707010051071aff0101620852005300e301770701000e0700ff0101622c52ff6301f4017707010000020000010101010630332e30300177070100605a0201010101010342bd01770701006161000001010101030000017707010060320104010101010850312e322e313201770701006032040401010101030422010101635256007605080e4fb56200620072630201710163fa1200001b1b1b1b1a01fd46";
104 |
105 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload));
106 |
107 | System.err.println(data);
108 |
109 | assertThat(data).isNotNull();
110 |
111 | assertThat(data.getMeterId()).isEqualTo("1EFR3347014668");
112 |
113 | assertThat(data.getReadings())
114 | .isNotNull()
115 | .extracting(Object::toString)
116 | .containsExactlyInAnyOrder(
117 | "1-0:1.8.0*255 / energyImportTotal = 2394350.4 WATT_HOUR",
118 | "1-0:2.8.0*255 / energyExportTotal = 4238700.3 WATT_HOUR",
119 | "1-0:16.7.0*255 / powerTotal = -9 WATT",
120 | "1-0:32.7.0*255 / voltageL1 = 235.3 VOLT",
121 | "1-0:52.7.0*255 / voltageL2 = 236.2 VOLT",
122 | "1-0:72.7.0*255 / voltageL3 = 235.9 VOLT",
123 | "1-0:31.7.0*255 / currentL1 = 1.04 AMPERE",
124 | "1-0:51.7.0*255 / currentL2 = 1.84 AMPERE",
125 | "1-0:71.7.0*255 / currentL3 = 1.16 AMPERE",
126 | "1-0:81.7.1*255 / phaseAngleUL2toUL1 = 121 DEGREE",
127 | "1-0:81.7.2*255 / phaseAngleUL3toUL1 = 238 DEGREE",
128 | "1-0:81.7.4*255 / phaseAngleIL1toUL1 = 264 DEGREE",
129 | "1-0:81.7.15*255 / phaseAngleIL2toUL2 = 288 DEGREE",
130 | "1-0:81.7.26*255 / phaseAngleIL3toUL3 = 227 DEGREE",
131 | "1-0:14.7.0*255 / networkFrequency = 50.0 HERTZ"
132 | );
133 |
134 | }
135 |
136 | @Test
137 | public void testSMLDecoderHYL_invalidMessage() throws Exception {
138 | // invalid SML structure (but "well known spec violation") - add a workaround
139 | String payload="1b1b1b1b0101010176040000016200620072650000010176010107000005e370330b0a01484c59020005dc4b010163d7210076040000026200620072650000070177010b0a01484c59020005dc4b01017c77070100603201010101010104484c590177070100600100ff010101010b0a01484c59020005dc4b0177070100010800ff65000000046505e37033621e52ff6504c2bfcc0177070100020800ff65000000046505e37033621e52ff65002d3d840177070100100700ff0101621b520052000177070100200700ff0101622352ff63090301770701001f0700ff0101622152fe62000177070100510704ff010162085200620001770701000e0700ff0101622c52ff6301f30177070100000200000101010109322e30312e3030310177070100605a02010101010105303246340177070100600500ff01010101650000000401010163682f00760400000362006200726500000201710163e8230000001b1b1b1b1a021af2";
140 |
141 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
142 |
143 | System.err.println(data);
144 |
145 | assertThat(data).isNotNull();
146 |
147 | assertThat(data.getMeterId()).isEqualTo("1HLY0200384075");
148 |
149 | assertThat(data.getReadings())
150 | .isNotNull()
151 | .extracting(Object::toString)
152 | .containsExactlyInAnyOrder(
153 | "1-0:1.8.0*255 / energyImportTotal = 7987194.8 WATT_HOUR",
154 | "1-0:2.8.0*255 / energyExportTotal = 296486.8 WATT_HOUR",
155 | "1-0:16.7.0*255 / powerTotal = 0 WATT",
156 | "1-0:32.7.0*255 / voltageL1 = 230.7 VOLT",
157 | "1-0:31.7.0*255 / currentL1 = 0.00 AMPERE",
158 | "1-0:81.7.4*255 / phaseAngleIL1toUL1 = 0 DEGREE",
159 | "1-0:14.7.0*255 / networkFrequency = 49.9 HERTZ"
160 | );
161 |
162 | }
163 |
164 | @Test
165 | public void testSMLDecoderEMH_ED300L() throws Exception {
166 | // a meter reported by a user as not decoded properly
167 |
168 | String payload="1b1b1b1b010101017607000c05de60f1620062007263010176010107000c0282cafb0b0901454d480000abd62a010163ea5f007607000c05de60f2620062007263070177010b0901454d480000abd62a070100620affff72620165028215f37e77078181c78203ff0101010104454d480177070100000000ff010101010f31454d48303031313236313438320177070100000009ff010101010b0901454d480000abd62a0177070100010800ff640102a001621e52ff5600034dd71f0177070100020800ff640102a001621e52ff560000116bbf0177070100010801ff0101621e52ff5600000000000177070100020801ff0101621e52ff560000116bbf0177070100010802ff0101621e52ff5600034dd71f0177070100020802ff0101621e52ff5600000000000177070100100700ff0101621b52ff55fffffdfe0177070100240700ff0101621b52ff55000000000177070100380700ff0101621b52ff55ffffed5301770701004c0700ff0101621b52ff55000010ab0177078181c78205ff010101018302c589dde68439c20d18cee93dbe78f31c5631ebd3c08854aca19472b39f1c2dd4179d445b05dfd68c3de8ca1a05bdc92c01010163d727007607000c05de60f562006200726302017101638e9e001b1b1b1b1a004c66";
169 |
170 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
171 |
172 | System.err.println(data);
173 |
174 | assertThat(data).isNotNull();
175 |
176 | assertThat(data.getMeterId()).isEqualTo("1EMH0011261482");
177 |
178 | assertThat(data.getReadings())
179 | .isNotNull()
180 | .extracting(Object::toString)
181 | .containsExactlyInAnyOrder(
182 | "1-0:1.8.0*255 / energyImportTotal = 5543299.1 WATT_HOUR",
183 | "1-0:2.8.0*255 / energyExportTotal = 114169.5 WATT_HOUR",
184 | "1-0:1.8.1*255 / energyImportTariff1 = 0.0 WATT_HOUR",
185 | "1-0:2.8.1*255 / energyExportTariff1 = 114169.5 WATT_HOUR",
186 | "1-0:1.8.2*255 / energyImportTariff2 = 5543299.1 WATT_HOUR",
187 | "1-0:2.8.2*255 / energyExportTariff2 = 0.0 WATT_HOUR",
188 | "1-0:16.7.0*255 / powerTotal = -51.4 WATT",
189 | "1-0:36.7.0*255 / powerL1 = 0.0 WATT",
190 | "1-0:56.7.0*255 / powerL2 = -478.1 WATT",
191 | "1-0:76.7.0*255 / powerL3 = 426.7 WATT"
192 | );
193 | }
194 |
195 | @Test
196 | public void testSMLDecoder_eHZ_PW8E2A6L0HQ2D() throws Exception {
197 | // Test for issue reported in https://github.com/micw/tibber-pulse-reader/issues/15
198 |
199 | String payload="1b1b1b1b0101010176050013f6ff6200620072630101760107ffffffffffff050006a7ab0b0a01454d480000c312a17262016406aea2620163b7fa0076050013f70062006200726307017707ffffffffffff0b0a01454d480000c312a1070100620affff7262016406aea27577070100603201010101010104454d480177070100600100ff010101010b0a01454d480000c312a10177070100010800ff640801047262016406aea2621e52ff6403e05a0177070100020800ff017262016406aea2621e52ff6367540177070100100700ff0101621b520052240101016305eb0076050013f7016200620072630201710163fd5c001b1b1b1b1a000d95";
200 |
201 | SMLMeterData data=SMLDecoder.decode(Hex.decodeHex(payload), false);
202 |
203 | System.err.println(data);
204 |
205 | assertThat(data).isNotNull();
206 |
207 | assertThat(data.getMeterId()).isEqualTo("1EMH0012784289");
208 |
209 | assertThat(data.getReadings())
210 | .isNotNull()
211 | .extracting(Object::toString)
212 | .containsExactlyInAnyOrder(
213 | "1-0:1.8.0*255 / energyImportTotal = 25404.2 WATT_HOUR",
214 | "1-0:2.8.0*255 / energyExportTotal = 2645.2 WATT_HOUR",
215 | "1-0:16.7.0*255 / powerTotal = 36 WATT"
216 | );
217 | }
218 |
219 |
220 |
221 | }
222 |
--------------------------------------------------------------------------------