Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Message Type Support #18

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ RUN apt update && \
rtl-sdr && \
rm -rf /var/lib/apt/lists/*
RUN go get github.com/bemasher/rtlamr
RUN python3 -V

# Copy files into place
COPY * /amridm2mqtt/
Expand Down
66 changes: 48 additions & 18 deletions README.Docker.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,76 @@
# Using amirdm2mqtt in Docker.
# Using amridm2mqtt in Docker

If you want to run this under Docker you can do so. A Dockerfile has been provided so you can build your own container.

## Building

Building should be a simple matter:

docker build -t amirdm2mqtt .
```shell
docker build -t amridm2mqtt .
```

## Configuration

All configuration for the docker container is handled through environment variables. You can pass these to `docker run` using the -e flag. At a minimum you need to set `WATCHED_METERS`.

| Environment Variable | Default | Required | Description |
|----------------------|----------|-------------|
|----------------------|---------|----------|-------------|
| WATCHED_METERS | | Yes | A comma or space separated list of meters to watch |
| WH_MULTIPLIER | 1000 | No | multiplier to get reading to Watt Hours (Wh) |
| READINGS_PER_HOUR | 12 | No | number of IDM intervals per hour reported by the meter |
| WH_MULTIPLIER | `1000` | No | multiplier to get reading to Watt Hours (Wh) |
| READINGS_PER_HOUR | `12` | No | number of IDM intervals per hour reported by the meter |
| MQTT_HOST | `127.0.0.1` | No | MQTT host to report to |
| MQTT_PORT | `1883' | No | MQTT port to use |
| MQTT_PORT | `1883` | No | MQTT port to use |
| MQTT_USER | | No | MQTT username for authentication |
| MQTT_PASSWORD | | No | MQTT password for authentication |
| MESSAGE_TYPE | `idm` | No | The message type rtlamr should output |
| DEBUG | `False` | No | Output debug messages |

## Running

In order to run your container will need to be both privileged and have a volume mount to `/dev/bus/usb`. You can do that by adding these arguments to `docker run`:

--privileged -v /dev/bus/usb:/dev/bus/usb
```shell
--privileged -v /dev/bus/usb:/dev/bus/usb
```

You may also need to give it access to the network for your mqtt server. If you have not yet set one up you can do so with these commands:

docker network create --attachable mqtt
docker network connect mqtt <mosquitto_container>
```shell
docker network create --attachable mqtt
docker network connect mqtt <mosquitto_container>
```

A comman `docker run` command incorporating the above advice along with a common configuration is provided as an example:

docker run -it --name amridm2mqtt \
--restart=unless-stopped \
--network=mqtt \
--privileged \
-v /dev/bus/usb:/dev/bus/usb \
-e WATCHED_METERS=12345678 \
-e READINGS_PER_HOUR=4 \
-e MQTT_HOST=mosquitto \
amridm2mqtt
```shell
docker run -it --name amridm2mqtt \
--restart=unless-stopped \
--network=mqtt \
--privileged \
-v /dev/bus/usb:/dev/bus/usb \
-e WATCHED_METERS=12345678 \
-e READINGS_PER_HOUR=4 \
-e MQTT_HOST=mosquitto \
amridm2mqtt
```

## Docker Compose

Here's an example using Docker Compose:

```yaml
amridm2mqtt:
container_name: amridm2mqtt
build: amridm2mqtt
environment:
- WATCHED_METERS=12345678
- READINGS_PER_HOUR=4
- MQTT_HOST=mosquitto
privileged: true
devices:
- /dev/bus/usb:/dev/bus/usb
depends_on:
- mosquitto
restart: unless-stopped
```
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# AMRIDM2MQTT: Send AMR/ERT Power Meter Data Over MQTT

##### Copyright (c) 2018 Ben Johnson. Distributed under MIT License.
## Copyright (c) 2018 Ben Johnson. Distributed under MIT License

Using an [inexpensive rtl-sdr dongle](https://www.amazon.com/s/ref=nb_sb_noss?field-keywords=RTL2832U), it's possible to listen for signals from ERT compatible smart meters using rtlamr. This script runs as a daemon, launches rtl_tcp and rtlamr, and parses the output from rtlamr. If this matches your meter, it will push the data into MQTT for consumption by Home Assistant, OpenHAB, or custom scripts.

TODO: Video for Home Assistant


## Docker

If you use Docker and would rather launch this under a container see <README.Docker.md>.
Expand Down Expand Up @@ -53,14 +52,13 @@ Install Go programming language & set gopath

`sudo apt-get install golang`

https://github.com/golang/go/wiki/SettingGOPATH
<https://github.com/golang/go/wiki/SettingGOPATH>

If only running go to get rtlamr, just set environment temporarily with the following command

`export GOPATH=$HOME/go`


Install rtlamr https://github.com/bemasher/rtlamr
Install rtlamr <https://github.com/bemasher/rtlamr>

`go get github.com/bemasher/rtlamr`

Expand All @@ -71,6 +69,7 @@ To make things convenient, I'm copying rtlamr to /usr/local/bin
## Install

### Clone Repo

Clone repo into opt

`cd /opt`
Expand Down Expand Up @@ -110,7 +109,8 @@ Set amridm2mqtt to run on startup
### Configure Home Assistant

To use these values in Home Assistant,
```

```yaml
sensor:
- platform: mqtt
state_topic: "readings/12345678/meter_reading"
Expand All @@ -121,7 +121,7 @@ sensor:
state_topic: "readings/12345678/meter_rate"
name: "Power Meter Avg Usage 5 mins"
unit_of_measurement: W
```
```

## Testing

Expand Down
185 changes: 117 additions & 68 deletions amridm2mqtt
Original file line number Diff line number Diff line change
Expand Up @@ -14,100 +14,149 @@ import subprocess
import signal
import sys
import time

import paho.mqtt.publish as publish
import settings
import messagetypes


# uses signal to shutdown and hard kill opened processes and self
def shutdown(signum, frame):
'''uses signal to shutdown and hard kill opened processes and self'''
rtltcp.send_signal(15)
rtlamr.send_signal(15)
time.sleep(1)
rtltcp.send_signal(9)
rtlamr.send_signal(9)
sys.exit(0)

signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)

# stores last interval id to avoid duplication, includes getter and setter
last_reading = {}

auth = None

if len(settings.MQTT_USER) and len(settings.MQTT_PASSWORD):
auth = {'username':settings.MQTT_USER, 'password':settings.MQTT_PASSWORD}

DEBUG=os.environ.get('DEBUG', '').lower() in ['1', 'true', 't']

def debug_print(*args, **kwargs):
if DEBUG:
print(*args, **kwargs)


def get_last_interval(meter_id):
return last_reading.get(meter_id, (None))


def set_last_interval(meter_id, interval_ID):
last_reading[meter_id] = (interval_ID)

# send data to MQTT broker defined in settings
def send_mqtt(topic, payload,):

def send_mqtt(topic, payload):
'''send data to MQTT broker defined in settings'''
try:
publish.single(topic, payload=payload, qos=1, hostname=settings.MQTT_HOST, port=settings.MQTT_PORT, auth=auth)
publish.single(topic, payload=payload, qos=1,
hostname=settings.MQTT_HOST, port=settings.MQTT_PORT, auth=auth)
except Exception as ex:
print("MQTT Publish Failed: " + str(ex))

# start the rtl_tcp program
rtltcp = subprocess.Popen([settings.RTL_TCP + " > /dev/null 2>&1 &"], shell=True,
stdin=None, stdout=None, stderr=None, close_fds=True)
time.sleep(5)

# start the rtlamr program.
rtlamr_cmd = [settings.RTLAMR, '-msgtype=idm', '-format=csv']
rtlamr = subprocess.Popen(rtlamr_cmd, stdout=subprocess.PIPE, universal_newlines=True)

while True:
try:
amrline = rtlamr.stdout.readline().strip()
flds = amrline.split(',')

if len(flds) != 66:
# proper IDM results have 66 fields
continue

# make sure the meter id is one we want
meter_id = int(flds[9])
if settings.WATCHED_METERS and meter_id not in settings.WATCHED_METERS:
continue

# get some required info: current meter reading, current interval id, most recent interval usage
read_cur = int(flds[15])
interval_cur = int(flds[10])
idm_read_cur = int(flds[16])

# retreive the interval id of the last time we sent to MQTT
def send_meter_reading(reading, meter):
current_reading_in_kwh = (reading * settings.WH_MULTIPLIER) / 1000
debug_print('Sending meter {} reading: {}'.format(
meter, current_reading_in_kwh))
send_mqtt('readings/{}/meter_reading'.format(meter),
str(current_reading_in_kwh))


def send_meter_usage(usage, meter):
rate = usage * settings.WH_MULTIPLIER * settings.READINGS_PER_HOUR
debug_print('Sending meter {} rate: {}'.format(meter, rate))
send_mqtt('readings/{}/meter_rate'.format(meter), str(rate))


def match_meterid(id):
if settings.WATCHED_METERS and id not in settings.WATCHED_METERS:
debug_print("meter id: ", id,
" doesn't match wanted meters: ", settings.WATCHED_METERS)
return False
return True


def parse_idm(flds):
# make sure the meter id is one we want
meter_id = int(flds[messagetypes.IDM_METER_ID])
if match_meterid(meter_id):
# get some required info:
# current meter reading
# current interval id
# most recent interval usage
current_reading = int(flds[messagetypes.IDM_CURRENT_READING])
current_interval = int(flds[messagetypes.IDM_CURRENT_INTERVAL])
interval_usage = int(flds[messagetypes.IDM_MOST_RECENT_INTERVAL_USAGE])
# retreive the interval id of the last time we sent data
interval_last = get_last_interval(meter_id)

if interval_cur != interval_last:

# as observed on on my meter...
# using values set in settings...
# each idm interval is 5 minutes (12x per hour),
# measured in hundredths of a kilowatt hour
# take the last interval usage times 10 to get watt-hours,
# then times 12 to get average usage in watts
rate = idm_read_cur * settings.WH_MULTIPLIER * settings.READINGS_PER_HOUR

current_reading_in_kwh = (read_cur * settings.WH_MULTIPLIER) / 1000

debug_print('Sending meter {} reading: {}'.format(meter_id, current_reading_in_kwh))
send_mqtt('readings/{}/meter_reading'.format(meter_id), str(current_reading_in_kwh))

debug_print('Sending meter {} rate: {}'.format(meter_id, rate))
send_mqtt('readings/{}/meter_rate'.format(meter_id), str(rate))

# if they don't match the current interval, send the data
if current_interval != interval_last:
send_meter_reading(current_reading, meter_id)
send_meter_usage(interval_usage, meter_id)
# store interval ID to avoid duplicating data
set_last_interval(meter_id, interval_cur)

except Exception as e:
debug_print('Exception squashed! {}: {}', e.__class__.__name__, e)
time.sleep(2)
set_last_interval(meter_id, current_interval)


def parse_scm(flds):
# make sure the meter id is one we want
meter_id = int(flds[messagetypes.SCM_METER_ID])
if match_meterid(meter_id):
# get some required info:
# current meter reading
current_reading = int(flds[messagetypes.SCM_CURRENT_READING])
# if they don't match the current interval, send the data
send_meter_reading(current_reading, meter_id)


if __name__ == "__main__":

DEBUG = settings.DEBUG

# Handle signals
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)

# stores last interval id to avoid duplication, includes getter and setter
last_reading = {}

# check and set our authentication
auth = None
if len(settings.MQTT_USER) and len(settings.MQTT_PASSWORD):
auth = {'username': settings.MQTT_USER,
'password': settings.MQTT_PASSWORD}

# start the rtl_tcp program
debug_print("Starting rtl_tcp...")
rtltcp = subprocess.Popen([settings.RTL_TCP + " > /dev/null 2>&1 &"], shell=True,
stdin=None, stdout=None, stderr=None, close_fds=True)
debug_print("Started rtl_tcp, waiting 5 seconds")
time.sleep(5)

# start the rtlamr program
rtlamr_cmd = [settings.RTLAMR,
f'-msgtype={settings.MESSAGE_TYPE}', '-format=csv']
debug_print("Starting rtlamr:", rtlamr_cmd)
rtlamr = subprocess.Popen(
rtlamr_cmd, stdout=subprocess.PIPE, universal_newlines=True)

debug_print("Processing rtlamr output")
while True:
try:
# read a line from the process stdout
amrline = rtlamr.stdout.readline().strip()
flds = amrline.split(',')
debug_print(amrline)

# Try to determine message type based on the number of fields
field_count = len(flds)
if field_count == messagetypes.IDM_FIELDS:
debug_print("Number of fields suggests message type idm, parsing...")
parse_idm(flds)
elif field_count == messagetypes.SCM_FIELDS:
debug_print("Number of fields suggests message type idm, parsing...")
parse_scm(flds)
else:
debug_print("Unsupported number of fields: ", field_count)
continue

except Exception as e:
debug_print('Exception squashed! {}: {}', e.__class__.__name__, e)
time.sleep(2)
Loading