Imported
This commit is contained in:
parent
5485196bf7
commit
ce362fde33
7 changed files with 1910 additions and 0 deletions
34
sensord.example
Executable file
34
sensord.example
Executable file
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/python3 -u
|
||||||
|
|
||||||
|
import sys, signal, os
|
||||||
|
# sys.path.append('/usr/local/lib/python3.5/dist-packages/sensord')
|
||||||
|
|
||||||
|
from sensord.RFM69receiver import RFM69Receiver, LogConsole
|
||||||
|
from sensord.InfluxDBWriter import InfluxDBWriter
|
||||||
|
from sensord.MQTT import MQTT
|
||||||
|
|
||||||
|
RFM_NODE = 1
|
||||||
|
RFM_NETW = 101
|
||||||
|
RFM_ENCR = '0123456789ABCDEF'
|
||||||
|
|
||||||
|
MQTT_HOST = "mqtt.broker"
|
||||||
|
MQTT_PORT = 8883
|
||||||
|
MQTT_BASE = "sensor/"
|
||||||
|
MQTT_USER = "sensor"
|
||||||
|
MQTT_PASS = "secretsauce"
|
||||||
|
MQTT_CERT = "/etc/mosquitto/ca_certificates/ca.crt"
|
||||||
|
|
||||||
|
INFLUX_HOST = "influxdb.host"
|
||||||
|
INFLUX_PORT = 8086
|
||||||
|
INFLUX_DB = "sensors"
|
||||||
|
|
||||||
|
rfm = RFM69Receiver(RFM_NODE, RFM_NETW, RFM_ENCR)
|
||||||
|
|
||||||
|
influx = InfluxDBWriter(INFLUX_HOST, INFLUX_PORT, INFLUX_DB)
|
||||||
|
mqtt = MQTT(MQTT_HOST, MQTT_PORT, MQTT_BASE, MQTT_USER, MQTT_PASS, MQTT_CERT)
|
||||||
|
|
||||||
|
rfm.attach_observer(influx)
|
||||||
|
rfm.attach_observer(mqtt)
|
||||||
|
rfm.attach_observer(LogConsole())
|
||||||
|
|
||||||
|
rfm.run()
|
43
sensord/InfluxDBWriter.py
Normal file
43
sensord/InfluxDBWriter.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
""" Module to Store Sensor Data into InfluxDB """
|
||||||
|
|
||||||
|
from influxdb import InfluxDBClient
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
class InfluxDBWriter(object):
|
||||||
|
""" Class to Represent the RRD Data of a Single Multi-Value Sensor """
|
||||||
|
|
||||||
|
def __init__(self, host, port=8086, dbname="sensors", user=None, passwd=None):
|
||||||
|
""" Initialize Class
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
host : InfluxDB Hostname (mandatory)
|
||||||
|
port: InfluxDB HTTP Port
|
||||||
|
dbname: Name of the InfluxDB database
|
||||||
|
user: Username
|
||||||
|
pass: Passwort (mandatory if user is set)
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.__host = host
|
||||||
|
self.__port = port
|
||||||
|
self.__dbname = dbname
|
||||||
|
if user != None and passwd != None:
|
||||||
|
self.__client = InfluxDBClient(host, port, user, passwd, dbname)
|
||||||
|
else:
|
||||||
|
self.__client = InfluxDBClient(host, port, database=dbname)
|
||||||
|
|
||||||
|
|
||||||
|
def notify(self, msg):
|
||||||
|
""" Notify Method Called from the Receiver Class, Inserts Values into InfluxDB
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg: Object Representation of the Sensor Data
|
||||||
|
"""
|
||||||
|
tmp = copy(msg)
|
||||||
|
json_body = [{"measurement": str(tmp['sid']), "fields": {}}]
|
||||||
|
tmp.pop('sid', None)
|
||||||
|
json_body[0]["fields"] = tmp
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.__client.write_points(json_body)
|
||||||
|
except:
|
||||||
|
print("Writing to InfluxDB Failed")
|
56
sensord/MQTT.py
Normal file
56
sensord/MQTT.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
""" Module to Publish Sensor Data to MQTT """
|
||||||
|
|
||||||
|
import paho.mqtt.client as paho
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
class MQTT(object):
|
||||||
|
""" Class to Represent a Client to a single MQTT Broker """
|
||||||
|
|
||||||
|
def __init__(self, host, port=1883, base="sensor/", user=None, passwd=None, certfile=None):
|
||||||
|
""" Initialize Class
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
host : MQTT Broker Hostname (mandatory)
|
||||||
|
port: MQTT Broker Port
|
||||||
|
base: Base MQTT Topic
|
||||||
|
user: Username
|
||||||
|
pass: Passwort (mandatory if user is set)
|
||||||
|
certfile: Path to CA-Cert File of the broker. TLS if used if set
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.__base = base
|
||||||
|
self.__client = paho.Client()
|
||||||
|
self.__client.on_connect = self.__on_connect
|
||||||
|
if user != None and passwd != None:
|
||||||
|
self.__client.username_pw_set(user, passwd)
|
||||||
|
if certfile != None:
|
||||||
|
self.__client.tls_set(certfile)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.__client.connect(host, port, 60)
|
||||||
|
self.__client.loop_start()
|
||||||
|
except:
|
||||||
|
print("Error Setting up MQTT Client")
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.__client.loop_stop()
|
||||||
|
self.__client.disconnect()
|
||||||
|
|
||||||
|
def __on_connect(self, client, userdata, ret, dummy):
|
||||||
|
""" Do Something when MQTT Connection is established """
|
||||||
|
print("Connected with result code "+str(ret))
|
||||||
|
|
||||||
|
def notify(self, msg):
|
||||||
|
""" Publish Sensor data to MQTT
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg: Object Representation of the Sensor Data
|
||||||
|
"""
|
||||||
|
tmp = copy(msg)
|
||||||
|
|
||||||
|
sid = tmp['sid']
|
||||||
|
tmp.pop('sid', None)
|
||||||
|
|
||||||
|
for stype, svalue in tmp.items():
|
||||||
|
self.__client.publish(self.__base + str(sid) + "/" + stype, str(svalue), 2, True)
|
378
sensord/RFM69.py
Normal file
378
sensord/RFM69.py
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
#!/usr/bin/env python2
|
||||||
|
|
||||||
|
from RFM69registers import *
|
||||||
|
import spidev
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import time
|
||||||
|
|
||||||
|
class RFM69(object):
|
||||||
|
def __init__(self, freqBand, nodeID, networkID, isRFM69HW = False, intPin = 18, rstPin = 28, spiBus = 0, spiDevice = 0):
|
||||||
|
|
||||||
|
self.freqBand = freqBand
|
||||||
|
self.address = nodeID
|
||||||
|
self.networkID = networkID
|
||||||
|
self.isRFM69HW = isRFM69HW
|
||||||
|
self.intPin = intPin
|
||||||
|
self.rstPin = rstPin
|
||||||
|
self.spiBus = spiBus
|
||||||
|
self.spiDevice = spiDevice
|
||||||
|
self.intLock = False
|
||||||
|
self.mode = ""
|
||||||
|
self.promiscuousMode = False
|
||||||
|
self.DATASENT = False
|
||||||
|
self.DATALEN = 0
|
||||||
|
self.SENDERID = 0
|
||||||
|
self.TARGETID = 0
|
||||||
|
self.PAYLOADLEN = 0
|
||||||
|
self.ACK_REQUESTED = 0
|
||||||
|
self.ACK_RECEIVED = 0
|
||||||
|
self.RSSI = 0
|
||||||
|
self.DATA = []
|
||||||
|
|
||||||
|
GPIO.setmode(GPIO.BOARD)
|
||||||
|
GPIO.setup(self.intPin, GPIO.IN)
|
||||||
|
GPIO.setup(self.rstPin, GPIO.OUT)
|
||||||
|
|
||||||
|
frfMSB = {RF69_315MHZ: RF_FRFMSB_315, RF69_433MHZ: RF_FRFMSB_433,
|
||||||
|
RF69_868MHZ: RF_FRFMSB_868, RF69_915MHZ: RF_FRFMSB_915}
|
||||||
|
frfMID = {RF69_315MHZ: RF_FRFMID_315, RF69_433MHZ: RF_FRFMID_433,
|
||||||
|
RF69_868MHZ: RF_FRFMID_868, RF69_915MHZ: RF_FRFMID_915}
|
||||||
|
frfLSB = {RF69_315MHZ: RF_FRFLSB_315, RF69_433MHZ: RF_FRFLSB_433,
|
||||||
|
RF69_868MHZ: RF_FRFLSB_868, RF69_915MHZ: RF_FRFLSB_915}
|
||||||
|
|
||||||
|
self.CONFIG = {
|
||||||
|
0x01: [REG_OPMODE, RF_OPMODE_SEQUENCER_ON | RF_OPMODE_LISTEN_OFF | RF_OPMODE_STANDBY],
|
||||||
|
#no shaping
|
||||||
|
0x02: [REG_DATAMODUL, RF_DATAMODUL_DATAMODE_PACKET | RF_DATAMODUL_MODULATIONTYPE_FSK | RF_DATAMODUL_MODULATIONSHAPING_00],
|
||||||
|
#default:4.8 KBPS
|
||||||
|
0x03: [REG_BITRATEMSB, RF_BITRATEMSB_55555],
|
||||||
|
0x04: [REG_BITRATELSB, RF_BITRATELSB_55555],
|
||||||
|
#default:5khz, (FDEV + BitRate/2 <= 500Khz)
|
||||||
|
0x05: [REG_FDEVMSB, RF_FDEVMSB_50000],
|
||||||
|
0x06: [REG_FDEVLSB, RF_FDEVLSB_50000],
|
||||||
|
|
||||||
|
0x07: [REG_FRFMSB, frfMSB[freqBand]],
|
||||||
|
0x08: [REG_FRFMID, frfMID[freqBand]],
|
||||||
|
0x09: [REG_FRFLSB, frfLSB[freqBand]],
|
||||||
|
|
||||||
|
# looks like PA1 and PA2 are not implemented on RFM69W, hence the max output power is 13dBm
|
||||||
|
# +17dBm and +20dBm are possible on RFM69HW
|
||||||
|
# +13dBm formula: Pout=-18+OutputPower (with PA0 or PA1**)
|
||||||
|
# +17dBm formula: Pout=-14+OutputPower (with PA1 and PA2)**
|
||||||
|
# +20dBm formula: Pout=-11+OutputPower (with PA1 and PA2)** and high power PA settings (section 3.3.7 in datasheet)
|
||||||
|
#0x11: [REG_PALEVEL, RF_PALEVEL_PA0_ON | RF_PALEVEL_PA1_OFF | RF_PALEVEL_PA2_OFF | RF_PALEVEL_OUTPUTPOWER_11111],
|
||||||
|
#over current protection (default is 95mA)
|
||||||
|
#0x13: [REG_OCP, RF_OCP_ON | RF_OCP_TRIM_95],
|
||||||
|
|
||||||
|
# RXBW defaults are { REG_RXBW, RF_RXBW_DCCFREQ_010 | RF_RXBW_MANT_24 | RF_RXBW_EXP_5} (RxBw: 10.4khz)
|
||||||
|
#//(BitRate < 2 * RxBw)
|
||||||
|
0x19: [REG_RXBW, RF_RXBW_DCCFREQ_010 | RF_RXBW_MANT_16 | RF_RXBW_EXP_2],
|
||||||
|
#for BR-19200: //* 0x19 */ { REG_RXBW, RF_RXBW_DCCFREQ_010 | RF_RXBW_MANT_24 | RF_RXBW_EXP_3 },
|
||||||
|
#DIO0 is the only IRQ we're using
|
||||||
|
0x25: [REG_DIOMAPPING1, RF_DIOMAPPING1_DIO0_01],
|
||||||
|
#must be set to dBm = (-Sensitivity / 2) - default is 0xE4=228 so -114dBm
|
||||||
|
0x29: [REG_RSSITHRESH, 220],
|
||||||
|
#/* 0x2d */ { REG_PREAMBLELSB, RF_PREAMBLESIZE_LSB_VALUE } // default 3 preamble bytes 0xAAAAAA
|
||||||
|
0x2e: [REG_SYNCCONFIG, RF_SYNC_ON | RF_SYNC_FIFOFILL_AUTO | RF_SYNC_SIZE_2 | RF_SYNC_TOL_0],
|
||||||
|
#attempt to make this compatible with sync1 byte of RFM12B lib
|
||||||
|
0x2f: [REG_SYNCVALUE1, 0x2D],
|
||||||
|
#NETWORK ID
|
||||||
|
0x30: [REG_SYNCVALUE2, networkID],
|
||||||
|
0x37: [REG_PACKETCONFIG1, RF_PACKET1_FORMAT_VARIABLE | RF_PACKET1_DCFREE_OFF |
|
||||||
|
RF_PACKET1_CRC_ON | RF_PACKET1_CRCAUTOCLEAR_ON | RF_PACKET1_ADRSFILTERING_OFF],
|
||||||
|
#in variable length mode: the max frame size, not used in TX
|
||||||
|
0x38: [REG_PAYLOADLENGTH, 66],
|
||||||
|
#* 0x39 */ { REG_NODEADRS, nodeID }, //turned off because we're not using address filtering
|
||||||
|
#TX on FIFO not empty
|
||||||
|
0x3C: [REG_FIFOTHRESH, RF_FIFOTHRESH_TXSTART_FIFONOTEMPTY | RF_FIFOTHRESH_VALUE],
|
||||||
|
#RXRESTARTDELAY must match transmitter PA ramp-down time (bitrate dependent)
|
||||||
|
0x3d: [REG_PACKETCONFIG2, RF_PACKET2_RXRESTARTDELAY_2BITS | RF_PACKET2_AUTORXRESTART_ON | RF_PACKET2_AES_OFF],
|
||||||
|
#for BR-19200: //* 0x3d */ { REG_PACKETCONFIG2, RF_PACKET2_RXRESTARTDELAY_NONE | RF_PACKET2_AUTORXRESTART_ON | RF_PACKET2_AES_OFF }, //RXRESTARTDELAY must match transmitter PA ramp-down time (bitrate dependent)
|
||||||
|
#* 0x6F */ { REG_TESTDAGC, RF_DAGC_CONTINUOUS }, // run DAGC continuously in RX mode
|
||||||
|
# run DAGC continuously in RX mode, recommended default for AfcLowBetaOn=0
|
||||||
|
0x6F: [REG_TESTDAGC, RF_DAGC_IMPROVED_LOWBETA0],
|
||||||
|
0x00: [255, 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
#initialize SPI
|
||||||
|
self.spi = spidev.SpiDev()
|
||||||
|
self.spi.open(self.spiBus, self.spiDevice)
|
||||||
|
self.spi.max_speed_hz = 4000000
|
||||||
|
|
||||||
|
# Hard reset the RFM module
|
||||||
|
GPIO.output(self.rstPin, GPIO.HIGH);
|
||||||
|
time.sleep(0.1)
|
||||||
|
GPIO.output(self.rstPin, GPIO.LOW);
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
#verify chip is syncing?
|
||||||
|
while self.readReg(REG_SYNCVALUE1) != 0xAA:
|
||||||
|
self.writeReg(REG_SYNCVALUE1, 0xAA)
|
||||||
|
|
||||||
|
while self.readReg(REG_SYNCVALUE1) != 0x55:
|
||||||
|
self.writeReg(REG_SYNCVALUE1, 0x55)
|
||||||
|
|
||||||
|
#write config
|
||||||
|
for value in self.CONFIG.values():
|
||||||
|
self.writeReg(value[0], value[1])
|
||||||
|
|
||||||
|
self.encrypt(0)
|
||||||
|
self.setHighPower(self.isRFM69HW)
|
||||||
|
# Wait for ModeReady
|
||||||
|
while (self.readReg(REG_IRQFLAGS1) & RF_IRQFLAGS1_MODEREADY) == 0x00:
|
||||||
|
pass
|
||||||
|
|
||||||
|
GPIO.remove_event_detect(self.intPin)
|
||||||
|
GPIO.add_event_detect(self.intPin, GPIO.RISING, callback=self.interruptHandler)
|
||||||
|
|
||||||
|
def setFreqeuncy(self, FRF):
|
||||||
|
self.writeReg(REG_FRFMSB, FRF >> 16)
|
||||||
|
self.writeReg(REG_FRFMID, FRF >> 8)
|
||||||
|
self.writeReg(REG_FRFLSB, FRF)
|
||||||
|
|
||||||
|
def setMode(self, newMode):
|
||||||
|
if newMode == self.mode:
|
||||||
|
return
|
||||||
|
|
||||||
|
if newMode == RF69_MODE_TX:
|
||||||
|
self.writeReg(REG_OPMODE, (self.readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_TRANSMITTER)
|
||||||
|
if self.isRFM69HW:
|
||||||
|
self.setHighPowerRegs(True)
|
||||||
|
elif newMode == RF69_MODE_RX:
|
||||||
|
self.writeReg(REG_OPMODE, (self.readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_RECEIVER)
|
||||||
|
if self.isRFM69HW:
|
||||||
|
self.setHighPowerRegs(False)
|
||||||
|
elif newMode == RF69_MODE_SYNTH:
|
||||||
|
self.writeReg(REG_OPMODE, (self.readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_SYNTHESIZER)
|
||||||
|
elif newMode == RF69_MODE_STANDBY:
|
||||||
|
self.writeReg(REG_OPMODE, (self.readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_STANDBY)
|
||||||
|
elif newMode == RF69_MODE_SLEEP:
|
||||||
|
self.writeReg(REG_OPMODE, (self.readReg(REG_OPMODE) & 0xE3) | RF_OPMODE_SLEEP)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# we are using packet mode, so this check is not really needed
|
||||||
|
# but waiting for mode ready is necessary when going from sleep because the FIFO may not be immediately available from previous mode
|
||||||
|
while self.mode == RF69_MODE_SLEEP and self.readReg(REG_IRQFLAGS1) & RF_IRQFLAGS1_MODEREADY == 0x00:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.mode = newMode;
|
||||||
|
|
||||||
|
def sleep(self):
|
||||||
|
self.setMode(RF69_MODE_SLEEP)
|
||||||
|
|
||||||
|
def setAddress(self, addr):
|
||||||
|
self.address = addr
|
||||||
|
self.writeReg(REG_NODEADRS, self.address)
|
||||||
|
|
||||||
|
def setNetwork(self, networkID):
|
||||||
|
self.networkID = networkID
|
||||||
|
self.writeReg(REG_SYNCVALUE2, networkID)
|
||||||
|
|
||||||
|
def setPowerLevel(self, powerLevel):
|
||||||
|
if powerLevel > 31:
|
||||||
|
powerLevel = 31
|
||||||
|
self.powerLevel = powerLevel
|
||||||
|
self.writeReg(REG_PALEVEL, (self.readReg(REG_PALEVEL) & 0xE0) | self.powerLevel)
|
||||||
|
|
||||||
|
def canSend(self):
|
||||||
|
if self.mode == RF69_MODE_STANDBY:
|
||||||
|
self.receiveBegin()
|
||||||
|
return True
|
||||||
|
#if signal stronger than -100dBm is detected assume channel activity
|
||||||
|
elif self.mode == RF69_MODE_RX and self.PAYLOADLEN == 0 and self.readRSSI() < CSMA_LIMIT:
|
||||||
|
self.setMode(RF69_MODE_STANDBY)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send(self, toAddress, buff = "", requestACK = False):
|
||||||
|
self.writeReg(REG_PACKETCONFIG2, (self.readReg(REG_PACKETCONFIG2) & 0xFB) | RF_PACKET2_RXRESTART)
|
||||||
|
now = time.time()
|
||||||
|
while (not self.canSend()) and time.time() - now < RF69_CSMA_LIMIT_S:
|
||||||
|
self.receiveDone()
|
||||||
|
self.sendFrame(toAddress, buff, requestACK, False)
|
||||||
|
|
||||||
|
# to increase the chance of getting a packet across, call this function instead of send
|
||||||
|
# and it handles all the ACK requesting/retrying for you :)
|
||||||
|
# The only twist is that you have to manually listen to ACK requests on the other side and send back the ACKs
|
||||||
|
# The reason for the semi-automaton is that the lib is ingterrupt driven and
|
||||||
|
# requires user action to read the received data and decide what to do with it
|
||||||
|
# replies usually take only 5-8ms at 50kbps@915Mhz
|
||||||
|
|
||||||
|
def sendWithRetry(self, toAddress, buff = "", retries = 3, retryWaitTime = 10):
|
||||||
|
for i in range(0, retries):
|
||||||
|
self.send(toAddress, buff, True)
|
||||||
|
sentTime = time.time()
|
||||||
|
while (time.time() - sentTime) * 1000 < retryWaitTime:
|
||||||
|
if self.ACKReceived(toAddress):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ACKReceived(self, fromNodeID):
|
||||||
|
if self.receiveDone():
|
||||||
|
return (self.SENDERID == fromNodeID or fromNodeID == RF69_BROADCAST_ADDR) and self.ACK_RECEIVED
|
||||||
|
return False
|
||||||
|
|
||||||
|
def ACKRequested(self):
|
||||||
|
return self.ACK_REQUESTED and self.TARGETID != RF69_BROADCAST_ADDR
|
||||||
|
|
||||||
|
def sendACK(self, toAddress = 0, buff = ""):
|
||||||
|
toAddress = toAddress if toAddress > 0 else self.SENDERID
|
||||||
|
while not self.canSend():
|
||||||
|
self.receiveDone()
|
||||||
|
self.sendFrame(toAddress, buff, False, True)
|
||||||
|
|
||||||
|
def sendFrame(self, toAddress, buff, requestACK, sendACK):
|
||||||
|
#turn off receiver to prevent reception while filling fifo
|
||||||
|
self.setMode(RF69_MODE_STANDBY)
|
||||||
|
#wait for modeReady
|
||||||
|
while (self.readReg(REG_IRQFLAGS1) & RF_IRQFLAGS1_MODEREADY) == 0x00:
|
||||||
|
pass
|
||||||
|
# DIO0 is "Packet Sent"
|
||||||
|
self.writeReg(REG_DIOMAPPING1, RF_DIOMAPPING1_DIO0_00)
|
||||||
|
|
||||||
|
if (len(buff) > RF69_MAX_DATA_LEN):
|
||||||
|
buff = buff[0:RF69_MAX_DATA_LEN]
|
||||||
|
|
||||||
|
ack = 0
|
||||||
|
if sendACK:
|
||||||
|
ack = 0x80
|
||||||
|
elif requestACK:
|
||||||
|
ack = 0x40
|
||||||
|
if isinstance(buff, str):
|
||||||
|
self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 3, toAddress, self.address, ack] + [int(ord(i)) for i in list(buff)])
|
||||||
|
else:
|
||||||
|
self.spi.xfer2([REG_FIFO | 0x80, len(buff) + 3, toAddress, self.address, ack] + buff)
|
||||||
|
|
||||||
|
startTime = time.time()
|
||||||
|
self.DATASENT = False
|
||||||
|
self.setMode(RF69_MODE_TX)
|
||||||
|
while not self.DATASENT:
|
||||||
|
if time.time() - startTime > 1.0:
|
||||||
|
break
|
||||||
|
self.setMode(RF69_MODE_RX)
|
||||||
|
|
||||||
|
def interruptHandler(self, pin):
|
||||||
|
self.intLock = True
|
||||||
|
self.DATASENT = True
|
||||||
|
if self.mode == RF69_MODE_RX and self.readReg(REG_IRQFLAGS2) & RF_IRQFLAGS2_PAYLOADREADY:
|
||||||
|
self.setMode(RF69_MODE_STANDBY)
|
||||||
|
self.PAYLOADLEN, self.TARGETID, self.SENDERID, CTLbyte = self.spi.xfer2([REG_FIFO & 0x7f,0,0,0,0])[1:]
|
||||||
|
if self.PAYLOADLEN > 66:
|
||||||
|
self.PAYLOADLEN = 66
|
||||||
|
if not (self.promiscuousMode or self.TARGETID == self.address or self.TARGETID == RF69_BROADCAST_ADDR):
|
||||||
|
self.PAYLOADLEN = 0
|
||||||
|
self.intLock = False
|
||||||
|
return
|
||||||
|
self.DATALEN = self.PAYLOADLEN - 3
|
||||||
|
self.ACK_RECEIVED = CTLbyte & 0x80
|
||||||
|
self.ACK_REQUESTED = CTLbyte & 0x40
|
||||||
|
|
||||||
|
self.DATA = self.spi.xfer2([REG_FIFO & 0x7f] + [0 for i in range(0, self.DATALEN)])[1:]
|
||||||
|
|
||||||
|
self.RSSI = self.readRSSI()
|
||||||
|
self.intLock = False
|
||||||
|
|
||||||
|
def receiveBegin(self):
|
||||||
|
|
||||||
|
while self.intLock:
|
||||||
|
time.sleep(.1)
|
||||||
|
self.DATALEN = 0
|
||||||
|
self.SENDERID = 0
|
||||||
|
self.TARGETID = 0
|
||||||
|
self.PAYLOADLEN = 0
|
||||||
|
self.ACK_REQUESTED = 0
|
||||||
|
self.ACK_RECEIVED = 0
|
||||||
|
self.RSSI = 0
|
||||||
|
if (self.readReg(REG_IRQFLAGS2) & RF_IRQFLAGS2_PAYLOADREADY):
|
||||||
|
# avoid RX deadlocks
|
||||||
|
self.writeReg(REG_PACKETCONFIG2, (self.readReg(REG_PACKETCONFIG2) & 0xFB) | RF_PACKET2_RXRESTART)
|
||||||
|
#set DIO0 to "PAYLOADREADY" in receive mode
|
||||||
|
self.writeReg(REG_DIOMAPPING1, RF_DIOMAPPING1_DIO0_01)
|
||||||
|
self.setMode(RF69_MODE_RX)
|
||||||
|
|
||||||
|
def receiveDone(self):
|
||||||
|
if (self.mode == RF69_MODE_RX or self.mode == RF69_MODE_STANDBY) and self.PAYLOADLEN > 0:
|
||||||
|
self.setMode(RF69_MODE_STANDBY)
|
||||||
|
return True
|
||||||
|
if self.readReg(REG_IRQFLAGS1) & RF_IRQFLAGS1_TIMEOUT:
|
||||||
|
# https://github.com/russss/rfm69-python/blob/master/rfm69/rfm69.py#L112
|
||||||
|
# Russss figured out that if you leave alone long enough it times out
|
||||||
|
# tell it to stop being silly and listen for more packets
|
||||||
|
self.writeReg(REG_PACKETCONFIG2, (self.readReg(REG_PACKETCONFIG2) & 0xFB) | RF_PACKET2_RXRESTART)
|
||||||
|
elif self.mode == RF69_MODE_RX:
|
||||||
|
# already in RX no payload yet
|
||||||
|
return False
|
||||||
|
self.receiveBegin()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def readRSSI(self, forceTrigger = False):
|
||||||
|
rssi = 0
|
||||||
|
if forceTrigger:
|
||||||
|
self.writeReg(REG_RSSICONFIG, RF_RSSI_START)
|
||||||
|
while self.readReg(REG_RSSICONFIG) & RF_RSSI_DONE == 0x00:
|
||||||
|
pass
|
||||||
|
rssi = self.readReg(REG_RSSIVALUE) * -1
|
||||||
|
rssi = rssi >> 1
|
||||||
|
return rssi
|
||||||
|
|
||||||
|
def encrypt(self, key):
|
||||||
|
self.setMode(RF69_MODE_STANDBY)
|
||||||
|
if key != 0 and len(key) == 16:
|
||||||
|
self.spi.xfer([REG_AESKEY1 | 0x80] + [int(ord(i)) for i in list(key)])
|
||||||
|
self.writeReg(REG_PACKETCONFIG2,(self.readReg(REG_PACKETCONFIG2) & 0xFE) | RF_PACKET2_AES_ON)
|
||||||
|
else:
|
||||||
|
self.writeReg(REG_PACKETCONFIG2,(self.readReg(REG_PACKETCONFIG2) & 0xFE) | RF_PACKET2_AES_OFF)
|
||||||
|
|
||||||
|
def readReg(self, addr):
|
||||||
|
return self.spi.xfer([addr & 0x7F, 0])[1]
|
||||||
|
|
||||||
|
def writeReg(self, addr, value):
|
||||||
|
self.spi.xfer([addr | 0x80, value])
|
||||||
|
|
||||||
|
def promiscuous(self, onOff):
|
||||||
|
self.promiscuousMode = onOff
|
||||||
|
|
||||||
|
def setHighPower(self, onOff):
|
||||||
|
if onOff:
|
||||||
|
self.writeReg(REG_OCP, RF_OCP_OFF)
|
||||||
|
#enable P1 & P2 amplifier stages
|
||||||
|
self.writeReg(REG_PALEVEL, (self.readReg(REG_PALEVEL) & 0x1F) | RF_PALEVEL_PA1_ON | RF_PALEVEL_PA2_ON)
|
||||||
|
else:
|
||||||
|
self.writeReg(REG_OCP, RF_OCP_ON)
|
||||||
|
#enable P0 only
|
||||||
|
self.writeReg(REG_PALEVEL, RF_PALEVEL_PA0_ON | RF_PALEVEL_PA1_OFF | RF_PALEVEL_PA2_OFF | powerLevel)
|
||||||
|
|
||||||
|
def setHighPowerRegs(self, onOff):
|
||||||
|
if onOff:
|
||||||
|
self.writeReg(REG_TESTPA1, 0x5D)
|
||||||
|
self.writeReg(REG_TESTPA2, 0x7C)
|
||||||
|
else:
|
||||||
|
self.writeReg(REG_TESTPA1, 0x55)
|
||||||
|
self.writeReg(REG_TESTPA2, 0x70)
|
||||||
|
|
||||||
|
def readAllRegs(self):
|
||||||
|
results = []
|
||||||
|
for address in range(1, 0x50):
|
||||||
|
results.append([str(hex(address)), str(bin(self.readReg(address)))])
|
||||||
|
return results
|
||||||
|
|
||||||
|
def readTemperature(self, calFactor):
|
||||||
|
self.setMode(RF69_MODE_STANDBY)
|
||||||
|
self.writeReg(REG_TEMP1, RF_TEMP1_MEAS_START)
|
||||||
|
while self.readReg(REG_TEMP1) & RF_TEMP1_MEAS_RUNNING:
|
||||||
|
pass
|
||||||
|
# COURSE_TEMP_COEF puts reading in the ballpark, user can add additional correction
|
||||||
|
#'complement'corrects the slope, rising temp = rising val
|
||||||
|
return (int(~self.readReg(REG_TEMP2)) * -1) + COURSE_TEMP_COEF + calFactor
|
||||||
|
|
||||||
|
|
||||||
|
def rcCalibration(self):
|
||||||
|
self.writeReg(REG_OSC1, RF_OSC1_RCCAL_START)
|
||||||
|
while self.readReg(REG_OSC1) & RF_OSC1_RCCAL_DONE == 0x00:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
self.setHighPower(False)
|
||||||
|
self.sleep()
|
||||||
|
GPIO.cleanup()
|
149
sensord/RFM69receiver.py
Normal file
149
sensord/RFM69receiver.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
"""
|
||||||
|
rfm69receiver - A RFM69 433MHz Receiver Using the Observer Pattern
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import struct
|
||||||
|
from pprint import pprint
|
||||||
|
import RFM69
|
||||||
|
from RFM69registers import RF69_433MHZ
|
||||||
|
|
||||||
|
class RFM69Receiver(object):
|
||||||
|
""" Receives Messages from a RFM69 Wireless SPI Transceiver and Can Notify
|
||||||
|
Multiple Observers to Process the Messages
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, nodeID, netID, encKey=None):
|
||||||
|
""" Initialize the Transceiver Module
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
nodeID: ID of the Node in the Network, typically 1 for the Receiver, between 1 and 255
|
||||||
|
netID: Network ID, all Nodes must use the same ID to communicate, between 1 and 255
|
||||||
|
encKey: 16 byte Encryption Key (AES-128). If None no encryption is used
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.__observers = []
|
||||||
|
self.__running = False
|
||||||
|
self.__lastsid = None
|
||||||
|
self.__lastts = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Initializing RFM69 Module as Node " + str(nodeID) + " on Network " + str(netID))
|
||||||
|
self.__rfm = RFM69.RFM69(RF69_433MHZ, nodeID, netID, False, rstPin=22)
|
||||||
|
self.__rfm.rcCalibration()
|
||||||
|
if encKey != None:
|
||||||
|
self.__rfm.encrypt(encKey)
|
||||||
|
except:
|
||||||
|
print("ERROR: Could not Initialize RFM69 Module")
|
||||||
|
raise RuntimeError
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
""" Free the Receiver Module Resources on Exit """
|
||||||
|
|
||||||
|
print("Shutting Down RFM69 Module")
|
||||||
|
self.__rfm.shutdown()
|
||||||
|
def _rawbytes(self, s):
|
||||||
|
"""Convert a string to raw bytes without encoding"""
|
||||||
|
outlist = []
|
||||||
|
for cp in s:
|
||||||
|
num = ord(cp)
|
||||||
|
if num < 255:
|
||||||
|
outlist.append(struct.pack('B', num))
|
||||||
|
elif num < 65535:
|
||||||
|
outlist.append(struct.pack('>H', num))
|
||||||
|
else:
|
||||||
|
b = (num & 0xFF0000) >> 16
|
||||||
|
H = num & 0xFFFF
|
||||||
|
outlist.append(struct.pack('>bH', b, H))
|
||||||
|
return b''.join(outlist)
|
||||||
|
|
||||||
|
|
||||||
|
def _process(self, sid, data, rssi):
|
||||||
|
""" Process received Data and Notify Registered Observers
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
sid: NodeID of the Sending Sensor
|
||||||
|
data: Data String or byte array
|
||||||
|
rssi: Signal Strength Indicator
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.__lastsid == sid and (time.time() - self.__lastts) < 10:
|
||||||
|
self.__lastsid = sid
|
||||||
|
self.__lastts = time.time()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.__lastsid = sid
|
||||||
|
self.__lastts = time.time()
|
||||||
|
msg = {}
|
||||||
|
msg['sid'] = sid
|
||||||
|
msg['rssi'] = rssi
|
||||||
|
if (data.find(';') != -1 and data.find('=') != -1):
|
||||||
|
values = data.split(";")
|
||||||
|
for value in values:
|
||||||
|
vsplit = value.split("=")
|
||||||
|
if vsplit[0] == "t" or vsplit[0] == "h" or vsplit[0] == "p":
|
||||||
|
msg[vsplit[0]] = int(vsplit[1].rstrip())/100
|
||||||
|
elif vsplit[0] == "v":
|
||||||
|
msg[vsplit[0]] = int(vsplit[1].rstrip())/1000
|
||||||
|
else:
|
||||||
|
msg[vsplit[0]] = int(vsplit[1].rstrip())
|
||||||
|
else:
|
||||||
|
bdata = self._rawbytes(data)
|
||||||
|
msg['t'] = int.from_bytes(bdata[0:3], byteorder='little', signed=True)/100
|
||||||
|
msg['p'] = int.from_bytes(bdata[4:7], byteorder='little', signed=True)/100
|
||||||
|
msg['h'] = int.from_bytes(bdata[8:11], byteorder='little', signed=True)/100
|
||||||
|
msg['v'] = int.from_bytes(bdata[12:15], byteorder='little', signed=True)/1000
|
||||||
|
|
||||||
|
for observer in self.__observers:
|
||||||
|
observer.notify(msg)
|
||||||
|
|
||||||
|
def attach_observer(self, obs):
|
||||||
|
""" Add an Object to the List of Known Observers """
|
||||||
|
|
||||||
|
self.__observers.append(obs)
|
||||||
|
|
||||||
|
def clear_observers(self):
|
||||||
|
""" Delete all Known Observers """
|
||||||
|
|
||||||
|
self.__observers.clear()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
""" Start the Receiving Loop """
|
||||||
|
|
||||||
|
self.__running = True
|
||||||
|
while self.__running:
|
||||||
|
self.__rfm.receiveBegin()
|
||||||
|
while not self.__rfm.receiveDone():
|
||||||
|
time.sleep(.1)
|
||||||
|
|
||||||
|
sid = self.__rfm.SENDERID
|
||||||
|
rssi = self.__rfm.RSSI
|
||||||
|
|
||||||
|
data = ""
|
||||||
|
for char in self.__rfm.DATA:
|
||||||
|
data += chr(char)
|
||||||
|
|
||||||
|
if self.__rfm.ACKRequested():
|
||||||
|
self.__rfm.sendACK()
|
||||||
|
|
||||||
|
if rssi != 0:
|
||||||
|
self._process(sid, data, rssi)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
""" Stop the Receiving Loop """
|
||||||
|
|
||||||
|
self.__running = False
|
||||||
|
|
||||||
|
class LogConsole(object):
|
||||||
|
""" This is an Example Observer Class That Logs received Sensor Data to STDOUT """
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
""" NOOP, does nothing """
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def notify(msg):
|
||||||
|
""" Pretty-Print the received Data """
|
||||||
|
|
||||||
|
print('[' + time.strftime('%H:%M:%S %d %b %Y') + '] Message Received: ', end='')
|
||||||
|
pprint(msg)
|
1093
sensord/RFM69registers.py
Normal file
1093
sensord/RFM69registers.py
Normal file
File diff suppressed because it is too large
Load diff
157
sensord/RRD.py
Normal file
157
sensord/RRD.py
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
""" Module to Store Sensor Data into an RRD Database and update Corresponding Graphs """
|
||||||
|
|
||||||
|
import _thread, time
|
||||||
|
import rrdtool
|
||||||
|
|
||||||
|
class RRDDB(object):
|
||||||
|
""" Class to Represent the RRD Data of a Single Multi-Value Sensor """
|
||||||
|
|
||||||
|
def __init__(self, rrdfile, sid, imgpath=None, sensor_type="thp"):
|
||||||
|
""" Initialize Class
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
rrdfile: Path to the RRD Database File (mandatory)
|
||||||
|
sid: Sensor ID which this RRD Database is for (mandatory)
|
||||||
|
imgpath: Path to Store Images, if set Images will be updated on call of notify()
|
||||||
|
sensor_type: Any combination of the following Letters:
|
||||||
|
t - Temperature
|
||||||
|
h - Humidity
|
||||||
|
p - Pressure
|
||||||
|
Default is 'thp' for a sensor sending all 3 values
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.__rrdfile = rrdfile
|
||||||
|
self.__sid = sid
|
||||||
|
self.__imgpath = imgpath
|
||||||
|
self.__t = bool('t' in sensor_type)
|
||||||
|
self.__h = bool('h' in sensor_type)
|
||||||
|
self.__p = bool('p' in sensor_type)
|
||||||
|
|
||||||
|
def create(self, interval):
|
||||||
|
""" Create a new RRD Database According to the Specified Sensor Types
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
interval: Seconds between Sensor Updates
|
||||||
|
"""
|
||||||
|
|
||||||
|
datasrc = []
|
||||||
|
datasrc.append('DS:v:GAUGE:' + str(interval*2) + ':U:U')
|
||||||
|
datasrc.append('DS:r:GAUGE:' + str(interval*2) + ':U:U')
|
||||||
|
if self.__t:
|
||||||
|
datasrc.append('DS:t:GAUGE:' + str(interval*2) + ':U:U')
|
||||||
|
if self.__h:
|
||||||
|
datasrc.append('DS:h:GAUGE:' + str(interval*2) + ':U:U')
|
||||||
|
if self.__p:
|
||||||
|
datasrc.append('DS:p:GAUGE:' + str(interval*2) + ':U:U')
|
||||||
|
|
||||||
|
rrdtool.create(self.__rrdfile,
|
||||||
|
'--step', str(interval),
|
||||||
|
datasrc,
|
||||||
|
'RRA:LAST:0.5:1:105120',
|
||||||
|
'RRA:AVERAGE:0.5:6:175680',
|
||||||
|
'RRA:MAX:0.5:6:175680',
|
||||||
|
'RRA:MIN:0.5:6:175680')
|
||||||
|
|
||||||
|
def notify(self, msg):
|
||||||
|
""" Notify Method Called from the Receiver Class, Inserts Values into the RRD Database
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg: Object Representation of the Sensor Data
|
||||||
|
"""
|
||||||
|
|
||||||
|
if msg['sid'] == self.__sid:
|
||||||
|
updstr = 'N:' + str(msg['v']) + ':' + str(msg['rssi'])
|
||||||
|
if self.__t:
|
||||||
|
updstr = updstr + ':' + str(msg['t'])
|
||||||
|
if self.__h:
|
||||||
|
updstr = updstr + ':' + str(msg['h'])
|
||||||
|
if self.__p:
|
||||||
|
updstr = updstr + ':' + str(msg['p'])
|
||||||
|
rrdtool.update(self.__rrdfile, updstr)
|
||||||
|
|
||||||
|
if self.__imgpath != None:
|
||||||
|
_thread.start_new_thread(self.update_web, ())
|
||||||
|
|
||||||
|
def _graph(self, value, start):
|
||||||
|
""" Draw a Graph for a Specified Sensor Value
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
value: Sensor Identifier (t, h, p, v or r)
|
||||||
|
start: When to Start the Graph (24h, 7d, 30d, 1y)
|
||||||
|
"""
|
||||||
|
|
||||||
|
types = {'t': ['Temperatur', '#FF0000', '°C'],
|
||||||
|
'h': ['Luftfeuchte', '#0000FF', '%%'],
|
||||||
|
'p': ['Luftdruck', '#00FF00', 'hPa'],
|
||||||
|
'v': ['Batterie', '#FFFF00', 'V'],
|
||||||
|
'r': ['RSSI', '#00FFFF', '']}
|
||||||
|
times = {'24h': ['Letzte 24h', 'LAST'],
|
||||||
|
'7d': ['Letzte 7 Tage', 'AVERAGE'],
|
||||||
|
'30d': ['Letzte 30 Tage', 'AVERAGE'],
|
||||||
|
'1y': ['Letzte 12 Monate', 'AVERAGE']}
|
||||||
|
|
||||||
|
rrdtool.graph(self.__imgpath + '/graph_' + value + '_' + start + '.png',
|
||||||
|
'-s', 'now-' + start, '-e', 'now',
|
||||||
|
'-t', times[start][0], '-z', '-A',
|
||||||
|
'DEF:val=' + self.__rrdfile + ':'+ value + ':' + times[start][1],
|
||||||
|
'VDEF:valmax=val,MAXIMUM',
|
||||||
|
'VDEF:valmin=val,MINIMUM',
|
||||||
|
'VDEF:valavg=val,AVERAGE',
|
||||||
|
'LINE2:val'+types[value][1]+':'+types[value][0],
|
||||||
|
'GPRINT:valavg:Avg %2.2lf '+types[value][2],
|
||||||
|
'GPRINT:valmin:Min %2.2lf '+types[value][2],
|
||||||
|
'GPRINT:valmax:Max %2.2lf '+types[value][2])
|
||||||
|
|
||||||
|
def update_web(self):
|
||||||
|
""" Renew RRD Graphs and HTML for the Sensor Values """
|
||||||
|
|
||||||
|
if self.__t:
|
||||||
|
self._graph('t', '24h')
|
||||||
|
self._graph('t', '7d')
|
||||||
|
self._graph('t', '30d')
|
||||||
|
self._graph('t', '1y')
|
||||||
|
if self.__h:
|
||||||
|
self._graph('h', '24h')
|
||||||
|
self._graph('h', '7d')
|
||||||
|
self._graph('h', '30d')
|
||||||
|
self._graph('h', '1y')
|
||||||
|
if self.__p:
|
||||||
|
self._graph('p', '24h')
|
||||||
|
self._graph('p', '7d')
|
||||||
|
self._graph('p', '30d')
|
||||||
|
self._graph('p', '1y')
|
||||||
|
|
||||||
|
last = rrdtool.lastupdate(self.__rrdfile)
|
||||||
|
|
||||||
|
with open(self.__imgpath + '/index.html', 'w') as html:
|
||||||
|
html.write('<html>\n<head>\n')
|
||||||
|
html.write('<link rel="stylesheet" type="text/css" href="theme.css">\n')
|
||||||
|
html.write('<meta http-equiv="refresh" content="300">')
|
||||||
|
html.write('<title>Sensorlog RBS 3.OG</title>\n</head>')
|
||||||
|
html.write('<body>\n<div id="header">\n')
|
||||||
|
html.write('<h1>Sensorlog RBS 3.OG</h1></div>\n<div id="content">')
|
||||||
|
if self.__t:
|
||||||
|
html.write('<div><h3>Temperatur</h3>')
|
||||||
|
html.write('<p id="current">Aktuell ' + str(last['ds']['t']) + ' °C</p>')
|
||||||
|
html.write('<img src="./graph_t_24h.png" alt="Daily Graph" />')
|
||||||
|
html.write('<img src="./graph_t_7d.png" alt="Weekly Graph" />')
|
||||||
|
html.write('<img src="./graph_t_30d.png" alt="Monthly Graph" />')
|
||||||
|
html.write('<img src="./graph_t_1y.png" alt="Yearly Graph" /></div>\n')
|
||||||
|
if self.__h:
|
||||||
|
html.write('<div><h3>Luftfeuchte</h3>')
|
||||||
|
html.write('<p id="current">Aktuell ' + str(last['ds']['h']) + ' %</p>')
|
||||||
|
html.write('<img src="./graph_h_24h.png" alt="Daily Graph" />')
|
||||||
|
html.write('<img src="./graph_h_7d.png" alt="Weekly Graph" />')
|
||||||
|
html.write('<img src="./graph_h_30d.png" alt="Monthly Graph" />')
|
||||||
|
html.write('<img src="./graph_h_1y.png" alt="Yearly Graph" /></div>\n')
|
||||||
|
if self.__p:
|
||||||
|
html.write('<div><h3>Luftdruck</h3>')
|
||||||
|
html.write('<p id="current">Aktuell ' + str(last['ds']['p']) + ' hPa</p>')
|
||||||
|
html.write('<img src="./graph_p_24h.png" alt="Daily Graph" />')
|
||||||
|
html.write('<img src="./graph_p_7d.png" alt="Weekly Graph" />')
|
||||||
|
html.write('<img src="./graph_p_30d.png" alt="Monthly Graph" />')
|
||||||
|
html.write('<img src="./graph_p_1y.png" alt="Yearly Graph" /></div>\n')
|
||||||
|
if float(last['ds']['v']) < 2.5:
|
||||||
|
html.write('<p class="warning">Batterien sind fast leer, bitte Wechseln!</p>')
|
||||||
|
html.write('<p>Letztes Update: '+time.strftime('%d.%m.%Y %H:%M:%S')+' | Batteriespannung: '+str(last['ds']['v'])+'V </p>')
|
||||||
|
html.write('</div></body></html>')
|
Loading…
Reference in a new issue