Skip to content

Instantly share code, notes, and snippets.

@nl-hugo
Last active December 26, 2023 10:13
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nl-hugo/acf9ceabb9a813d067484d9723ca3f77 to your computer and use it in GitHub Desktop.
Save nl-hugo/acf9ceabb9a813d067484d9723ca3f77 to your computer and use it in GitHub Desktop.
How to set up Smart Metering with Raspberry Pi zero, InfluxDB and Grafana
tmp*
_elk
_old

references:

https://hostpresto.com/community/tutorials/how-to-install-influxdb-on-ubuntu-14-04/

official guide https://docs.influxdata.com/influxdb/v1.1/introduction/installation/

Raspberry Pi (zero)

The setup:

  • Raspberry Pi zero
  • Raspbian Jessy image
  • 64GB SD card
  • wifi dongle
  • P1 cable

Raspberry Pi zero

InfluxDB

InfluxDB is a time series database specifically built to store large amounts of timestamped data. We're going to use its retention policies and continuous queries to automatically aggregate and expire the ingested metering data in order to built an efficient Smart Metering dashboard.

Let's follow the official guide to install the latest version for Debian. Add the InfluxData repository, but be aware that you might need to run sudo apt-get install apt-transport-https first, if it's not already installed on your system:

curl -sL https://repos.influxdata.com/influxdb.key | sudo apt-key add -
source /etc/os-release
test $VERSION_ID = "7" && echo "deb https://repos.influxdata.com/debian wheezy stable" | sudo tee /etc/apt/sources.list.d/influxdb.list
test $VERSION_ID = "8" && echo "deb https://repos.influxdata.com/debian jessie stable" | sudo tee /etc/apt/sources.list.d/influxdb.list

Then, install and run the InfluxDB service

sudo apt-get update && sudo apt-get install influxdb
sudo systemctl start influxdb

Test if the service is listening to the network ports

$ sudo netstat -naptu | grep LISTEN | grep influxd
tcp6       0      0 :::8088                 :::*                    LISTEN      1011/influxd
tcp6       0      0 :::8086                 :::*                    LISTEN      1011/influxd

And query the repository for listed databases:

$ curl -G 'http://localhost:8086/query?pretty=true' --data-urlencode "q=SHOW DATABASES"
{
    "results": [
        {
            "series": [
                {
                    "name": "databases",
                    "columns": [
                        "name"
                    ],
                    "values": [
                        [
                            "_internal"
                        ],
                        [
                            "p1smartmeter"
                        ]
                    ]
                }
            ]
        }
    ]
}

Configuration [TODO]

in conf file: enable admin panel (v1.1 disables it by default) ---> TODO: enable?? --> reporting off

Don't forget to restart the influx service for the changes to take effect.

sudo systemctl restart influxdb

Setting up the Smart Meter database with Influx CLI

Start the CLI by: influx -precision rfc3339

should open the cli

Create a database

Create the smartmeter db:

CREATE DATABASE "p1smartmeter"

Note that if you choose another name for the database, you need to provide it later on as argument when starting the python script.

Typing SHOW DATABASES should now return the following:

> SHOW DATABASES
name: databases
name
----
_internal
p1smartmeter

Now enter the database:

USE p1smartmeter

Creating retention policies

Retention policies (RP) and continuous queries (CQ; see https://docs.influxdata.com/influxdb/v1.1/guides/downsampling_and_retention/) are used to downsample (CQ) and persist (RP) the data for a specified amount of time.

Three retention policies are created for data storage: 30 days, 6 months and infinity:

CREATE RETENTION POLICY "30_days" ON "p1smartmeter" DURATION 30d REPLICATION 1 DEFAULT;
CREATE RETENTION POLICY "6_months" ON "p1smartmeter" DURATION 26w REPLICATION 1;
CREATE RETENTION POLICY "infinite" ON "p1smartmeter" DURATION INF REPLICATION 1;

See all retention policies:

> SHOW RETENTION POLICIES
name            duration        shardGroupDuration      replicaN        default
----            --------        ------------------      --------        -------
autogen         0s              168h0m0s                1               false
30_days         720h0m0s        24h0m0s                 1               true
6_months        4368h0m0s       168h0m0s                1               false
infinite        0s              168h0m0s                1               false

Creating continuous queries

Raw data from the smart meter is captured every 10 seconds. The raw data is kept for 30 days under the default "30_days" RP. Every 15 minutes, the following CQ "cq_smartmeter_hourly" downsamples the raw data into hourly data, which is stored for 6 months as enforced by the "6_months" RP.

CREATE CONTINUOUS QUERY "cq_smartmeter_hourly" ON "p1smartmeter" RESAMPLE EVERY 15m BEGIN SELECT min(*), max(*), spread(*) INTO "6_months"."smartmeter_hourly" FROM "30_days"."smartmeter" GROUP BY time(1h),* END

Every hour, another QC "cq_smartmeter_daily" downsamples the raw data into daily chunks and stores it for an infinite amount of time in the "infinite" RP.

CREATE CONTINUOUS QUERY "cq_smartmeter_daily" ON "p1smartmeter" RESAMPLE EVERY 1h BEGIN SELECT min(*), max(*), spread(*) INTO "infinite"."smartmeter_daily" FROM "30_days"."smartmeter" GROUP BY time(1d),* END

Finally, I created python script that calls a remote weather service to get outside temperatures. This is useful for relating gas consumption to the weather. The weather data is stored indefinitely in hourly blocks by the "cq_weather_daily" CQ:

CREATE CONTINUOUS QUERY "cq_weather_daily" ON "p1smartmeter" RESAMPLE EVERY 1h BEGIN SELECT min(*), max(*), mean(*) INTO "infinite"."weather_daily" FROM "30_days"."weather" GROUP BY time(1d),* END

Show all continuous queries:

> SHOW CONTINUOUS QUERIES
name: p1smartmeter
name                    query
----                    -----
cq_smartmeter_hourly    CREATE CONTINUOUS QUERY cq_smartmeter_hourly ON p1smartmeter RESAMPLE EVERY 15m BEGIN SELECT min(*), max(*), spread(*) INTO p1smartmeter."6_months".smartmeter_hourly FROM p1smartmeter."30_days".smartmeter GROUP BY time(1h), * END
cq_smartmeter_daily     CREATE CONTINUOUS QUERY cq_smartmeter_daily ON p1smartmeter RESAMPLE EVERY 1h BEGIN SELECT min(*), max(*), spread(*) INTO p1smartmeter.infinite.smartmeter_daily FROM p1smartmeter."30_days".smartmeter GROUP BY time(1d), * END
cq_weather_daily        CREATE CONTINUOUS QUERY cq_weather_daily ON p1smartmeter RESAMPLE EVERY 1h BEGIN SELECT min(*), max(*), mean(*) INTO p1smartmeter.infinite.weather_daily FROM p1smartmeter."30_days".weather GROUP BY time(1d), * END

type exit to quit.

install python dependencies

http://influxdb-python.readthedocs.io/en/latest/include-readme.html

sudo apt-get install python-influxdb
sudo easy_install pip
sudo pip2 install --upgrade influxdb

Note that the installation through apt-get as suggested did not work for me. It throws an 404 error at the client.create_database('example'). See also http://stackoverflow.com/questions/36846975/influxdb-python-404-page-not-found

get script from https://bitbucket.org/frankiepankie/random-scripts/src/95d1334ef839d3cec8004a651fea641676247300/P1SmartmeterToInfluxDB/smartmeter-influxdb.py?at=master&fileviewer=file-view-default

check the config / run the script

Make sure you add any variables as argument that you chosse not to be default.

systemd file maken /etc/systemd/system/smartmeter.service

[Unit] Description=smartmeter

[Service] Type=simple User=hugo ExecStart=/usr/bin/python2 /usr/share/dsmr/smartmeter-influxdb.py -b 115200 -q -d /dev/ttyUSB0 Restart=always

[Install] WantedBy=multi-user.target

systemctl enable and start sudo systemctl enable smartmeter && sudo systemctl start smartmeter

check status sudo systemctl status smartmeter

set access rights on dialout group??

Grafana

Grafana is ...

As there are no official Debian packages available, it makes the installation of Grafana on a Raspberry Pi not as straightforward as the InfluxDB installation. However, this repository provides unofficial deb packages specifically build for Raspberry Pi.

[CHECK] Be sure to check your Pi's architecture by running uname -a first. For a Rasperry Pi zero (which returns armv6) you can use the following commands. For other Pi versions, check the wiki for the correct repository.

sudo apt-get install adduser libfontconfig
curl -L https://dl.bintray.com/fg2it/deb-rpi-1b/main/g/grafana_4.0.2-1481228314_armhf.deb -o /tmp/grafana_4.0.2-1481228314_armhf.deb
sudo dpkg -i /tmp/grafana_4.0.2-1481228314_armhf.deb

Now enable and start the grafana server

sudo systemctl daemon-reload
sudo systemctl enable grafana-server && sudo systemctl start grafana-server

The dashboard is accessible through port 3000. The default credentials are admin/admin.

Configuration [TODO]

cd /etc/grafana sudo nano grafana.ini

(http://docs.grafana.org/installation/configuration/) edit [auth.anonymous] enabled Set to true to enable anonymous access. Defaults to false

In the gui set the default dashboards for the organization

sudo systemctl restart grafana-server.service

Dashboard [TODO]

download dashboard here

menu: data sources Add data source

name: optionally check default type: InfluxDB url: http://localhost:8086

database: p1smartmeter user pass

And please switch to the light theme.

OpenWeather API

http://bigl.es/using-python-to-get-weather-data/

Remove again??

sudo apt-get remove python-pip
sudo easy_install pip
sudo pip2 install --upgrade pyowm

wget https://gist.githubusercontent.com/nl-hugo/acf9ceabb9a813d067484d9723ca3f77/raw/owm-influxdb.py

cron

*/15 * * * * /usr/bin/python2 /usr/share/dsmr/owm-influxdb.py --api-key="[your key here]" --location=2751320 -v

SELECT * FROM "infinite".weather_daily ORDER BY time desc limit 10

http://nl-hugo.roughdraft.io/acf9ceabb9a813d067484d9723ca3f77

{
"id": 2,
"title": "Smart meter",
"description": "Smart meter",
"tags": [],
"style": "dark",
"timezone": "browser",
"editable": true,
"sharedCrosshair": false,
"hideControls": false,
"time": {
"from": "now-3h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": false,
"schemaVersion": 13,
"version": 22,
"links": [],
"gnetId": null,
"rows": [
{
"title": "Electra",
"panels": [
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"datasource": "smartmeter",
"editable": true,
"error": false,
"format": "kwatth",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"id": 3,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"span": 2,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"1h"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "smartmeter",
"policy": "autogen",
"query": "SELECT mean(\"+T\") FROM \"autogen\".\"smartmeter\" WHERE $timeFilter GROUP BY time(1h) fill(null)",
"rawQuery": false,
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"+T1"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"thresholds": "",
"title": "T1 (nacht)",
"transparent": true,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"datasource": null,
"editable": true,
"error": false,
"format": "kwatth",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"id": 4,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"span": 2,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"1h"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "smartmeter",
"policy": "autogen",
"query": "SELECT mean(\"P\") FROM \"autogen\".\"smartmeter\" WHERE $timeFilter GROUP BY time(1h) fill(null)",
"rawQuery": false,
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"+T2"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"thresholds": "",
"title": "T2 (dag)",
"transparent": true,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"aliasColors": {},
"bars": false,
"datasource": "smartmeter",
"editable": true,
"error": false,
"fill": 1,
"hideTimeOverride": false,
"id": 1,
"legend": {
"alignAsTable": true,
"avg": false,
"current": true,
"hideEmpty": false,
"max": true,
"min": true,
"rightSide": false,
"show": false,
"total": false,
"values": true
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 4,
"stack": false,
"steppedLine": false,
"targets": [
{
"alias": "huidig",
"dsType": "influxdb",
"groupBy": [
{
"params": [
"1m"
],
"type": "time"
}
],
"measurement": "smartmeter",
"policy": "autogen",
"query": "SELECT \"+P\" FROM \"autogen\".\"smartmeter\" WHERE $timeFilter",
"rawQuery": false,
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"+P"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Verbruik (kWh)",
"tooltip": {
"msResolution": false,
"shared": false,
"sort": 0,
"value_type": "individual"
},
"transparent": true,
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": [
"total"
]
},
"yaxes": [
{
"format": "none",
"label": "",
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "kwatth",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
]
},
{
"aliasColors": {},
"bars": true,
"datasource": null,
"editable": true,
"error": false,
"fill": 1,
"hideTimeOverride": false,
"id": 6,
"interval": "",
"legend": {
"alignAsTable": false,
"avg": false,
"current": false,
"max": false,
"min": false,
"show": false,
"total": false,
"values": false
},
"lines": false,
"linewidth": 1,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 4,
"stack": false,
"steppedLine": false,
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"24h"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "smartmeter",
"policy": "autogen",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"P"
],
"type": "field"
},
{
"params": [],
"type": "sum"
}
]
],
"tags": []
}
],
"thresholds": [],
"timeFrom": "7d",
"timeShift": null,
"title": "Verbruik (kWh)",
"tooltip": {
"msResolution": false,
"shared": false,
"sort": 0,
"value_type": "individual"
},
"transparent": true,
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": [
"min"
]
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
]
}
],
"showTitle": true,
"titleSize": "h6",
"height": "250px",
"repeat": null,
"repeatRowId": null,
"repeatIteration": null,
"collapse": false
},
{
"title": "Gas",
"panels": [
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"datasource": null,
"editable": true,
"error": false,
"format": "m3",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"id": 5,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"span": 6,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"1h"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "smartmeter",
"policy": "autogen",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"G"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": []
}
],
"thresholds": "",
"title": "Gas",
"transparent": true,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
}
],
"showTitle": true,
"titleSize": "h6",
"height": 250,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null,
"collapse": false
}
]
}
'''
Send OpenWeatherMap data to an InfluxDB API.
author: nl-hugo
date: 30-12-2016
see: http://openweathermap.org/current#current_JSON
usage: python .\owm-influxdb.py --api-key="<api key>" --location=<location id>
'''
import pyowm
class OWMData(object):
def __init__(self, key, location):
self._key = key
self._location = location
def read_weather(self):
observation = b''
owm = pyowm.OWM(self._key)
obs = owm.weather_at_id(self._location)
return Observation(obs)
class Observation(object):
_observation = ''
def __init__(self, observation):
self._observation = observation
w = self._observation.get_weather()
keys = {}
# TODO: rain and snow
keys['location'] = self._observation.get_location().get_name()
keys['temperature'] = w.get_temperature('celsius')['temp']
keys['humidity'] = w.get_humidity() / 100
keys['pressure'] = w.get_pressure()['press']
keys['clouds'] = w.get_clouds() / 100
keys['wind.speed'] = w.get_wind()['speed']
keys['wind.degree'] = w.get_wind()['deg']
self._keys = keys
self._timestamp = self._observation.get_reception_time()
def send_to_influxdb(options, fields, timestamp):
from influxdb import InfluxDBClient
req = {
"measurement": options.influx_measurement,
"tags": {},
"fields": {},
"timestamp": timestamp
}
if options.influx_tags is not None:
for tag in options.influx_tags:
tag_kv = tag.split('=')
req['tags'][tag_kv[0]] = tag_kv[1]
for field_k, field_v in fields.iteritems(): # python2!
if field_v is not None:
req['fields'][field_k] = field_v
reqs = []
reqs.append(req)
client = InfluxDBClient(options.influx_hostname, options.influx_port, options.influx_username, options.influx_password, options.influx_database)
client.write_points(reqs, retention_policy=options.influx_retention_policy, database=options.influx_database)
def start_monitor(options):
meter = OWMData(options.api_key, options.location)
try:
observation = meter.read_weather()
if options.verbose:
print(observation._keys)
send_to_influxdb(options, observation._keys, observation._timestamp)
finally:
pass
def main(argv=None):
from argparse import ArgumentParser
parser = ArgumentParser(description="Send OpenWeatherMap data to an InfluxDB API")
parser.add_argument("-k", "--api-key", dest="api_key", help="key for OpenWeatherMap API")
parser.add_argument("-l", "--location", dest="location", help="location id for the weather update, defaults to 2745912 (Utrecht,NL)", type=int, default=2745912)
influx_group = parser.add_argument_group()
influx_group.add_argument("--influx-hostname", metavar='hostname', dest="influx_hostname", help="hostname to connect to InfluxDB, defaults to 'localhost'", default="localhost")
influx_group.add_argument("--influx-port", metavar='port', dest="influx_port", help="port to connect to InfluxDB, defaults to 8086", type=int, default=8086)
influx_group.add_argument("--influx-username", metavar='username', dest="influx_username", help="user to connect, defaults to 'root'", default="root")
influx_group.add_argument("--influx-password", metavar='password', dest="influx_password", help="password of the user, defaults to 'root'", default="root")
influx_group.add_argument("--influx-database", metavar='dbname', dest="influx_database", help="database name to connect to, defaults to 'p1smartmeter'", default="p1smartmeter")
influx_group.add_argument("--influx-retention-policy", metavar='policy', dest="influx_retention_policy", help="retention policy to use")
influx_group.add_argument("--influx-measurement", metavar='measurement', dest="influx_measurement", help="measurement name to store points, defaults to weather", default="weather")
influx_group.add_argument('influx_tags', metavar='tag ...', type=str, nargs='?', help='any tag to the measurement')
verbose_group = parser.add_mutually_exclusive_group()
verbose_group.add_argument("-v", "--verbose", action="store_true", dest="verbose", help="Be verbose")
verbose_group.add_argument("-q", "--quiet", action="store_true", dest="quiet", help="Be very quiet")
args = parser.parse_args()
start_monitor(args)
if __name__ == "__main__":
import sys
sys.exit(main())
'''
smartmeter -- Send P1 telegram to an InfluxDB API.
Credits for the meter reading part (+ parsing and CRC) go to https://github.com/nrocco/smeterd
https://bitbucket.org/frankiepankie/random-scripts/src/95d1334ef839d3cec8004a651fea641676247300/P1SmartmeterToInfluxDB/smartmeter-influxdb.py?at=master&fileviewer=file-view-default
'''
import decimal
import re
import serial
import crcmod.predefined
crc16 = crcmod.predefined.mkPredefinedCrcFun('crc16')
class SmartMeter(object):
def __init__(self, port, *args, **kwargs):
try:
self.serial = serial.Serial(
port,
kwargs.get('baudrate', 115200),
timeout=10,
bytesize=serial.SEVENBITS,
parity=serial.PARITY_EVEN,
stopbits=serial.STOPBITS_ONE
)
except (serial.SerialException,OSError) as e:
raise SmartMeterError(e)
else:
self.serial.setRTS(False)
self.port = self.serial.name
def connect(self):
if not self.serial.isOpen():
self.serial.open()
self.serial.setRTS(False)
def disconnect(self):
if self.serial.isOpen():
self.serial.close()
def connected(self):
return self.serial.isOpen()
def read_one_packet(self):
datagram = b''
lines_read = 0
startFound = False
endFound = False
max_lines = 35 #largest known telegram has 35 lines
while not startFound or not endFound:
try:
line = self.serial.readline()
except Exception as e:
raise SmartMeterError(e)
lines_read += 1
if re.match(b'.*(?=/)', line):
startFound = True
endFound = False
datagram = line.lstrip()
elif re.match(b'(?=!)', line):
endFound = True
datagram = datagram + line
else:
datagram = datagram + line
# TODO: build in some protection for infinite loops
return P1Packet(datagram)
class SmartMeterError(Exception):
pass
class P1PacketError(Exception):
pass
class P1Packet(object):
_datagram = ''
def __init__(self, datagram):
self._datagram = datagram
self.validate()
keys = {}
keys['+T1'] = self.get_float(b'^1-0:1\.8\.1\(([0-9]+\.[0-9]+)\*kWh\)\r\n')
keys['-T1'] = self.get_float(b'^1-0:2\.8\.1\(([0-9]+\.[0-9]+)\*kWh\)\r\n')
keys['+T2'] = self.get_float(b'^1-0:1\.8\.2\(([0-9]+\.[0-9]+)\*kWh\)\r\n')
keys['-T2'] = self.get_float(b'^1-0:2\.8\.2\(([0-9]+\.[0-9]+)\*kWh\)\r\n')
keys['+P'] = self.get_float(b'^1-0:1\.7\.0\(([0-9]+\.[0-9]+)\*kW\)\r\n')
keys['-P'] = self.get_float(b'^1-0:2\.7\.0\(([0-9]+\.[0-9]+)\*kW\)\r\n')
keys['+T'] = keys['+T1'] + keys['+T2']
keys['-T'] = keys['-T1'] + keys['-T2']
keys['P'] = keys['+P'] - keys['-P']
keys['G'] = self.get_float(b'^(?:0-1:24\.2\.1(?:\(\d+[SW]\))?)?\(([0-9]{5}\.[0-9]{3})(?:\*m3)?\)\r\n', 0)
self._keys = keys
def __getitem__(self, key):
return self._keys[key]
def get_float(self, regex, default=None):
result = self.get(regex, None)
if not result:
return default
return float(self.get(regex, default))
def get_int(self, regex, default=None):
result = self.get(regex, None)
if not result:
return default
return int(result)
def get(self, regex, default=None):
results = re.search(regex, self._datagram, re.MULTILINE)
if not results:
return default
return results.group(1).decode('ascii')
def validate(self):
pattern = re.compile(b'\r\n(?=!)')
for match in pattern.finditer(self._datagram):
packet = self._datagram[:match.end() + 1]
checksum = self._datagram[match.end() + 1:]
if checksum.strip():
given_checksum = int('0x' + checksum.decode('ascii').strip(), 16)
calculated_checksum = crc16(packet)
if given_checksum != calculated_checksum:
raise P1PacketError('P1Packet with invalid checksum found')
def __str__(self):
return self._datagram.decode('ascii')
def send_to_influxdb(options, fields):
from influxdb import InfluxDBClient
req = {
"measurement": options.influx_measurement,
"tags": {},
"fields": {}
}
if options.influx_tags is not None:
for tag in options.influx_tags:
tag_kv = tag.split('=')
req['tags'][tag_kv[0]] = tag_kv[1]
for field_k, field_v in fields.iteritems():
if field_v is not None:
req['fields'][field_k] = field_v
reqs = []
reqs.append(req)
client = InfluxDBClient(options.influx_hostname, options.influx_port, options.influx_username, options.influx_password, options.influx_database)
client.write_points(reqs, retention_policy=options.influx_retention_policy, database=options.influx_database)
def start_monitor(options):
meter = SmartMeter(options.device, options.baudrate)
try:
while True:
datagram = meter.read_one_packet()
send_to_influxdb(options, datagram._keys)
finally:
meter.disconnect()
def main(argv=None):
from argparse import ArgumentParser
parser = ArgumentParser(description="Send P1 telegrams to an InfluxDB API")
parser.add_argument("-d", "--device", dest="device", help="serial port to read datagrams from", default='/dev/ttyUSB0')
parser.add_argument("-b", "--baudrate", dest="baudrate", help="baudrate for the serial connection", default='115200')
influx_group = parser.add_argument_group()
influx_group.add_argument("--influx-hostname", metavar='hostname', dest="influx_hostname", help="hostname to connect to InfluxDB, defaults to 'localhost'", default="localhost")
influx_group.add_argument("--influx-port", metavar='port', dest="influx_port", help="port to connect to InfluxDB, defaults to 8086", type=int, default=8086)
influx_group.add_argument("--influx-username", metavar='username', dest="influx_username", help="user to connect, defaults to 'root'", default="root")
influx_group.add_argument("--influx-password", metavar='password', dest="influx_password", help="password of the user, defaults to 'root'", default="root")
influx_group.add_argument("--influx-database", metavar='dbname', dest="influx_database", help="database name to connect to, defaults to 'p1smartmeter'", default="p1smartmeter")
influx_group.add_argument("--influx-retention-policy", metavar='policy', dest="influx_retention_policy", help="retention policy to use")
influx_group.add_argument("--influx-measurement", metavar='measurement', dest="influx_measurement", help="measurement name to store points, defaults to smartmeter", default="smartmeter")
influx_group.add_argument('influx_tags', metavar='tag ...', type=str, nargs='?', help='any tag to the measurement')
verbose_group = parser.add_mutually_exclusive_group()
verbose_group.add_argument("-v", "--verbose", action="store_true", dest="verbose", help="Be verbose")
verbose_group.add_argument("-q", "--quiet", action="store_true", dest="quiet", help="Be very quiet")
args = parser.parse_args()
start_monitor(args)
if __name__ == "__main__":
import sys
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment