Mobile Alerts Sensors

ELV is selling affordable weather sensors http://www.elv.de/ip-wettersensoren-system.html, which can be monitored via an iOS application. To allow that all data is transmitted via a gateway to a server, which the application can access. Sadly the protocol is not documented. I don’t think anybody has documented the way the URL has been constructed, especially the MD5 hash over the first part of it.

This sample code integrates sensors into fhem and I wrote it for Mac OS X, but it should work on Linux, too.

All information (and more) is available on GitHub as well.

To integrate the sensors into fhem, add the sensors to the iOS application and check if they show up. You need to note all sensor IDs. The ID is 6 bytes long and represented as a hexadecimal number. This same code supports sensors of type 2 (temperature only) and type 3 (temperature and humidity), which can be recognized via the first two characters of the ID.

You have to adjust the following things in the code:

  • sensors = … Here you have to add all sensors IDs as strings
  • downloadPath This needs to be adjusted to point to the log folder of fhem.
  • vendorid You might want to change the UUID to a new one (just run uuidgen from the terminal)

Python Code

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import os
import json
import pprint
import datetime
import hashlib
import time

downloadPath = os.path.expanduser('~/fhem-5.7/log')
pp = pprint.PrettyPrinter()

sensors =  set(['021122334455',...,'031122334455'])

def parseJSON(jj):
    if not jj['success']:
        print '#%d %s' % (jj['errorcode'],jj['errormessage'])
        return
    for device in jj['result']['devices']:
        #pp.pprint(device)
        deviceTypeId = device['devicetypeid']
        deviceName = device['name']
        print '%s %s [%d] #%d %s' % (device['deviceid'], deviceName, device['lowbattery'],len(device['measurements']),datetime.datetime.fromtimestamp(device['lastseen']).strftime('%Y-%m-%d %H:%M:%S'))
        year = -1
        month = -1
        logPath = ''
        for measurement in device['measurements']:
            idx = measurement['idx']
            transmitTime = datetime.datetime.fromtimestamp(measurement['c'])
            measurementTime = datetime.datetime.fromtimestamp(measurement['ts'])
            tx = measurement['tx']
            mm = measurement
            del mm['idx']
            del mm['c']
            del mm['ts']
            del mm['tx']

            if year != measurementTime.year or month != measurementTime.month:
                if len(logPath):
                    f = open(logPath, "w")
                    for d in sorted(data):
                        f.write('%s %s\n' % (d,data[d]))
                year = measurementTime.year
                month = measurementTime.month
                logPath = downloadPath + '/S_SENSOR_%s-%d-%02d.log' % (deviceName.upper().replace(' ','_'),year,month)
                data = {}
                try:
                    for line in open(logPath).readlines():
                        vals = line[:-1].split(' ')
                        data[' '.join(vals[:-1])] = vals[-1]
                except:
                    pass
            dataKey = measurementTime.strftime('%Y-%m-%d_%H:%M:%S') + ' SENSOR_' + deviceName.upper().replace(' ','_') + ' '
            if deviceTypeId == 2:
                data[dataKey + 'TEMP:'] = '%.1f' % (measurement['t1'])
            elif deviceTypeId == 3:
                data[dataKey + 'TEMP:'] = '%.1f' % (measurement['t1'])
                data[dataKey + 'FEUCHTIGKEIT:'] = '%.1f' % (measurement['h'])
            else:
                pp.pprint(mm)
        if len(logPath):
            f = open(logPath, "w")
            for d in sorted(data):
                f.write('%s %s\n' % (d,data[d]))


import urllib
import urllib2

devicetoken = 'empty'               # defaults to "empty"
vendorid = '1FB220C4-CC15-4195-97CF-8BE4FD3DAE72'   # iOS vendor UUID (returned by iOS, any UUID will do). Launch uuidgen from the terminal to generate a fresh one.
phoneid = 'Unknown'                 # Phone ID - probably generated by the server based on the vendorid (this string can be "Unknown" and it still works)
version = '1.21'                    # Info.plist CFBundleShortVersionString
build = '248'                       # Info.plist CFBundleVersion
executable = 'Mobile Alerts'        # Info.plist CFBundleExecutable
bundle = 'de.synertronixx.remotemonitor'    # [[NSBundle mainBundle] bundleIdentifier]
lang = 'en'                         # preferred language

request = "devicetoken=%s&vendorid=%s&phoneid=%s&version=%s&build=%s&executable=%s&bundle=%s&lang=%s" % (devicetoken,vendorid,phoneid,version,build,executable,bundle,lang)
request += '&timezoneoffset=%d' % 60        # local offset to UTC time
request += '&timeampm=%s' % ('true')        # 12h vs 24h clock
request += '&usecelsius=%s' % ('true')      # Celcius vs Fahrenheit
request += '&usemm=%s' % ('true')           # mm va in
request += '&speedunit=%d' % 0              # wind speed (0: m/s, 1: km/h, 2: mph, 3: kn)
request += '&timestamp=%s' % datetime.datetime.utcnow().strftime("%s")  # current UTC timestamp

requestMD5 = request + 'asdfaldfjadflxgeteeiorut0ß8vfdft34503580'  # SALT for the MD5
requestMD5 = requestMD5.replace('-','')
requestMD5 = requestMD5.replace(',','')
requestMD5 = requestMD5.replace('.','')
requestMD5 = requestMD5.lower()

m = hashlib.md5()
m.update(requestMD5)
hexdig = m.hexdigest()

request += '&requesttoken=%s' % hexdig

request += '&deviceids=%s' % ','.join(sensors)
#request += '&measurementfroms=%s' % ('0,' * len(sensors))
#request += '&measurementcounts=%s' % ('50,' * len(sensors))

http_header = {
                "User-Agent" : "remotemonitor/248 CFNetwork/758.2.8 Darwin/15.0.0",
                "Accept-Language" : "en-us",
                "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
                "Host" : "www.data199.com:8080",
                }

# create an urllib2 opener()
opener = urllib2.build_opener()

# create your HTTP request
req = urllib2.Request('http://www.data199.com:8080/api/v1/dashboard', request, http_header)

# submit your request
while True:
    res = opener.open(req)
    parseJSON(json.loads(res.read()))
    print '-' * 40
    time.sleep(10*60)   # wait for 10 minutes