Skip to content

Commit

Permalink
Add support for ingesting STATE telemetry data from Sonoff-Tasmota de…
Browse files Browse the repository at this point in the history
…vices
  • Loading branch information
amotl committed Jun 4, 2019
1 parent 77b94fa commit 5f5d951
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 15 deletions.
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ in progress
===========
- Fix Terkin telemetry client for PHP5 to PHP7. Thanks, Markus and Christian!
- Fix sandbox installation
- Add basic support for ingesting telemetry data from devices running
- Add basic support for ingesting SENSOR telemetry data from devices running
the Sonoff-Tasmota firmware. Thanks, Roh!
- Add support for ingesting STATE telemetry data from Sonoff-Tasmota devices.


.. _kotori-0.22.7:
Expand Down
9 changes: 9 additions & 0 deletions doc/source/applications/sonoff-tasmota.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,12 @@ Payload examples
},
"TempUnit": "C"
}

::

{
"Time": "2017-02-16T10:13:52",
"DS18B20": {
"Temperature": 20.6
}
}
17 changes: 17 additions & 0 deletions doc/source/applications/sonoff-tasmota/state-payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Time": "2019-06-02T22:13:07",
"Uptime": "1T18:10:35",
"Vcc": 3.182,
"SleepMode": "Dynamic",
"Sleep": 50,
"LoadAvg": 19,
"Wifi": {
"AP": 1,
"SSId": "{redacted}",
"BSSId": "A0:F3:C1:{redacted}",
"Channel": 1,
"RSSI": 100,
"LinkCount": 1,
"Downtime": "0T00:00:07"
}
}
Empty file added kotori/daq/devices/__init__.py
Empty file.
75 changes: 75 additions & 0 deletions kotori/daq/devices/tasmota.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# (c) 2019 Andreas Motl <andreas@getkotori.org>
import types
from collections import OrderedDict


class TasmotaDecoder:

def __init__(self, topology):
self.topology = topology

def decode_message(self, message):
# Fixme: Currently ignores the "Time" field.
if 'slot' in self.topology and self.topology.slot.endswith('SENSOR'):
message = self.decode_sensor_message(message)
if 'slot' in self.topology and self.topology.slot.endswith('STATE'):
message = self.decode_state_message(message)
return message

def decode_sensor_message(self, message):
"""
{
"Time": "2019-06-02T22:13:07",
"SonoffSC": {
"Temperature": 25,
"Humidity": 15,
"Light": 20,
"Noise": 10,
"AirQuality": 90
},
"TempUnit": "C"
}
{
"Time": "2017-02-16T10:13:52",
"DS18B20": {
"Temperature": 20.6
}
}
"""
data = OrderedDict()
for key, value in message.items():
if isinstance(value, types.DictionaryType):
data.update(value)
return data

def decode_state_message(self, message):
"""
{
"Time": "2019-06-02T22:13:07",
"Uptime": "1T18:10:35",
"Vcc": 3.182,
"SleepMode": "Dynamic",
"Sleep": 50,
"LoadAvg": 19,
"Wifi": {
"AP": 1,
"SSId": "{redacted}",
"BSSId": "A0:F3:C1:{redacted}",
"Channel": 1,
"RSSI": 100,
"LinkCount": 1,
"Downtime": "0T00:00:07"
}
}
"""
data = OrderedDict()
data['Device.Vcc'] = message.get('Vcc')
data['Device.Sleep'] = message.get('Sleep')
data['Device.LoadAvg'] = message.get('LoadAvg')
data['Device.Wifi.Channel'] = message.get('Wifi', {}).get('Channel')
data['Device.Wifi.RSSI'] = message.get('Wifi', {}).get('RSSI')
data['Device.Wifi.LinkCount'] = message.get('Wifi', {}).get('LinkCount')
return data
3 changes: 2 additions & 1 deletion kotori/daq/intercom/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ def topology_to_storage(self, topology):
# data: Regular endpoint
# loop: WeeWX
# SENSOR: Sonoff-Tasmota
if topology.slot.startswith('data') or topology.slot.startswith('loop') or topology.slot.endswith('SENSOR'):
if topology.slot.startswith('data') or topology.slot.startswith('loop') \
or topology.slot.endswith('SENSOR') or topology.slot.endswith('STATE'):
suffix = 'sensors'

elif topology.slot.startswith('event'):
Expand Down
23 changes: 10 additions & 13 deletions kotori/daq/services/mig.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
# -*- coding: utf-8 -*-
# (c) 2015-2018 Andreas Motl <andreas@getkotori.org>
import re
import time
import json
import types
from collections import OrderedDict

import arrow
from bunch import Bunch
Expand All @@ -15,6 +12,8 @@
from twisted.application.service import MultiService, Service
from twisted.python.failure import Failure
from twisted.python.threadpool import ThreadPool

from kotori.daq.devices.tasmota import TasmotaDecoder
from kotori.daq.services.schema import MessageType, TopicMatchers
from kotori.daq.services import MultiServiceMixin
from kotori.daq.intercom.mqtt import MqttAdapter
Expand Down Expand Up @@ -163,15 +162,14 @@ def process_message(self, topic, payload, **kwargs):
# Required for WeeWX data
#message = convert_floats(json.loads(payload))

# Decode Sonoff-Tasmota telemetry payload.
# Todo: Refactor to some device-specific knowledgebase.
# Fixme: Currently ignores the "Time" field.
if 'slot' in topology and topology.slot.endswith('/SENSOR'):
data = OrderedDict()
for key, value in message.items():
if isinstance(value, types.DictionaryType):
data.update(value)
message = data
decoders = [
# Decoder for Sonoff-Tasmota telemetry payload.
TasmotaDecoder,
]

for decoder_class in decoders:
decoder = decoder_class(topology=topology)
message = decoder.decode_message(message)

# b) Discrete values
#
Expand Down Expand Up @@ -199,7 +197,6 @@ def process_message(self, topic, payload, **kwargs):
value = float(payload)
message = {name: value}


# Set an event
elif message_type == MessageType.EVENT:

Expand Down
1 change: 1 addition & 0 deletions kotori/daq/services/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class TopicPatterns:

# Sonoff-Tasmota
'/SENSOR',
'/STATE',
]

discrete = [
Expand Down

0 comments on commit 5f5d951

Please sign in to comment.