Added thingsboard data

This commit is contained in:
Nico Melone
2025-01-26 10:34:54 -06:00
parent 6048c578a1
commit 6d09010fb3
4 changed files with 179 additions and 12 deletions

175
app.py
View File

@@ -1,17 +1,53 @@
import requests, os
import requests, os, pytz
import plotly.graph_objects as go
import plotly.express as px
from flask import Flask, render_template, jsonify, make_response
from weasyprint import HTML
import pandas as pd
from datetime import datetime as dt
from datetime import timedelta as td
import datetime as dtf
from tb_rest_client.rest_client_ce import *
from tb_rest_client.rest import ApiException
app = Flask(__name__)
@app.route('/report')
def report():
# Create Plotly graph
#fig = go.Figure(data=[go.Scatter(x=[1, 2, 3], y=[10, 20, 30], mode='lines')])
df = px.data.stocks()
fig = px.line(df, x='date', y=['AAPL', 'GOOG'])
#Get data for graph
df = getData()
# Create a figure with multiple y-axes
fig = go.Figure()
# Add the first line (INLET FLOW RATE) to the primary y-axis
fig.add_trace(go.Scatter(x=df.index, y=df['INLET FLOW RATE'],
mode='lines', name='Inlet Flow Rate',
yaxis='y1'))
# Add the second line (INLET TURBIDITY) to the secondary y-axis
fig.add_trace(go.Scatter(x=df.index, y=df['SALES FLOW RATE'],
mode='lines', name='Sales Flow Rate',
yaxis='y2'))
# Update layout for multiple y-axes
fig.update_layout(
#title='Graph with Multiple Y-Axes',
xaxis=dict(title='Time'),
yaxis=dict(
title='Flow Rate',
titlefont=dict(color='blue'),
tickfont=dict(color='blue')
),
yaxis2=dict(
title='Turbidity',
titlefont=dict(color='red'),
tickfont=dict(color='red'),
anchor='x',
overlaying='y',
side='right'
),
#legend=dict(x=0, y=1)
)
# Save the graph as an image
image_path = os.path.join('static', 'graph.svg')
fig.write_image(image_path)
@@ -47,5 +83,134 @@ def download_pdf():
response.headers['Content-Disposition'] = 'inline; filename=report.pdf'
return response
def getDataFrame(telemetry, ignore_keys, time):
df = pd.DataFrame()
#for location in telemetry.keys():
# Iterate through each datapoint within each location
for datapoint in telemetry.keys():
# Convert the datapoint list of dictionaries to a DataFrame
if datapoint not in ignore_keys:
temp_df = pd.DataFrame(telemetry[datapoint])
temp_df['ts'] = pd.to_datetime(temp_df['ts'], unit='ms').dt.tz_localize('UTC').dt.tz_convert(time["timezone"]).dt.tz_localize(None)
# Set 'ts' as the index
temp_df.set_index('ts', inplace=True)
temp_df["value"] = pd.to_numeric(temp_df["value"], errors="coerce")
# Rename 'value' column to the name of the datapoint
temp_df.rename(columns={'value': formatColumnName(datapoint)}, inplace=True)
# Join the temp_df to the main DataFrame
df = df.join(temp_df, how='outer')
df.ffill()
#df = df.fillna(method='ffill', limit=2)
# Rename index to 'Date'
df.rename_axis('Date', inplace=True)
return df
def formatColumnName(telemetryName):
name = " ".join([x.capitalize() for x in telemetryName.split("_")])
label_mapping = {
"Lit 116b Level": "WASTE TANK 1",
"Lit 116a Level": "WASTE TANK 2",
"Fit 100 Flow Rate": "INLET FLOW RATE",
"Fit 109b Flow Rate": "SALES FLOW RATE",
"Outlet Turbidity Temp": "OUTLET TURBIDITY TEMP",
"Outlet Orp Temp": "OUTLET ORP TEMP",
"Inlet Turbidity Temp": "INLET TURBIDITY TEMP",
"Inlet Ph Temp": "INLET PH TEMP",
"Ait 102b H2s": "INLET H₂S",
"At 109b H2s": "OUTLET H₂S",
"At 109c Oil In Water": "OUTLET OIL IN WATER",
"Ait 102a Turbitity": "INLET TURBIDITY",
"At 109a Turbidity": "OUTLET TURBIDITY",
"At 109e Orp": "OUTLET ORP"
}
return label_mapping.get(name)
def getThingsBoardData(url, username, password, targetCustomer, timeRequest):
# Creating the REST client object with context manager to get auto token refresh
with RestClientCE(base_url=url) as rest_client:
try:
# Auth with credentials
rest_client.login(username=username, password=password)
# Get customers > get devices under a target customer > get keys for devices > get data for devices
customers = rest_client.get_customers(page_size="100", page="0")
devices = getDevices(rest_client=rest_client, customers=customers, target_customer=targetCustomer)
telemetry = {}
for d in devices.data:
#print(d.name)
device, keys, err = getDeviceKeys(rest_client=rest_client, devices=devices, target_device=d.name)
start_ts, end_ts = getTime(timeRequest)
#print(keys)
telemetry[d.name] = getTelemetry(rest_client=rest_client, device=device, keys=','.join(keys), start_ts=start_ts, end_ts=end_ts, limit=25000)
return telemetry
except ApiException as e:
print(e)
return False
def getTime(timeRequest):
start_ts, end_ts = 0,0
if timeRequest["type"] == "last":
now = dt.now()
delta = td(days=timeRequest["days"], seconds=timeRequest["seconds"], microseconds=timeRequest["microseconds"], milliseconds=timeRequest["milliseconds"], minutes=timeRequest["minutes"], hours=timeRequest["hours"], weeks=timeRequest["weeks"])
start_ts = str(int(dt.timestamp(now - delta) * 1000))
end_ts = str(int(dt.timestamp(now) * 1000))
elif timeRequest["type"] == "midnight-midnight":
timezone = pytz.timezone(timeRequest["timezone"])
today = dtf.date.today()
yesterday_midnight = dtf.datetime.combine(today - dtf.timedelta(days=1), dtf.time())
today_midnight = dtf.datetime.combine(today, dtf.time())
yesterday_midnight = timezone.localize(yesterday_midnight)
today_midnight = timezone.localize(today_midnight)
start_ts = int(yesterday_midnight.timestamp()) * 1000
end_ts = int(today_midnight.timestamp()) * 1000
elif timeRequest["type"] == "range":
start_ts = timeRequest["ts_start"]
end_ts = timeRequest["ts_end"]
return (start_ts, end_ts)
def getTelemetry(rest_client, device, keys, start_ts, end_ts,limit):
try:
return rest_client.get_timeseries(entity_id=device.id, keys=keys, start_ts=start_ts, end_ts=end_ts, limit=limit) #entity_type=entity_type,
except Exception as e:
print("Something went wrong in getTelemetry")
print(e)
return False
def getDevices(rest_client, customers,target_customer, page=0, pageSize=500):
for c in customers.data:
if c.name == target_customer:
cid = c.id.id
devices = rest_client.get_customer_devices(customer_id=cid, page_size=pageSize, page=page)
return devices #.to_dict()
def getDeviceKeys(rest_client, devices,target_device):
try:
for d in devices.data:
if d.name == target_device:
device = d
keys = rest_client.get_timeseries_keys_v1(d.id)
return device, keys, None
return None, None,"Device Not Found"
except Exception as e:
print("Something went wrong in getDeviceKeys")
print(e)
return (None, None, e)
def getData():
# ThingsBoard REST API URL
url = "https://www.enxlekkocloud.com" #"https://hp.henrypump.cloud"
# Default Tenant Administrator credentials
username = "nico.a.melone@gmail.com" #"henry.pump.automation@gmail.com"
password = "9EE#mqb*b6bXV9hJrPYGm&w3q5Y@3acumvvb5isQ" #"Henry Pump @ 2022"
time = {
"type": "range",
"timezone": "US/Alaska" ,
"ts_start": 1728115200000,
"ts_end": 1728201600000
}
telemetry = getThingsBoardData(url, username, password, "Thunderbird Field Services", time)
ignore_keys = ['latitude', 'longitude', 'speed', 'a_current', 'b_current', 'c_current', 'scada_stop_cmd', 'pit_100a_pressure', 'pit_101a_pressure', 'pit_101b_pressure', 'pit_101c_pressure', 'fit_101_flow_rate', 'fi_101b_popoff', 'fcv_101a_valve', 'fcv_101b_valve', 'pit_102_pressure', 'pit_102_hi_alm', 'pit_102_hihi_alm', 'pit_102_hi_spt', 'pit_102_hihi_spt', 'p200_hand', 'p200_auto', 'xy_200_run', 'ct_200_run', 'pit_100_pressure', 'm106a_vfd_active', 'm106a_vfd_faulted', 'm106a_vfd_frequency', 'm106a_vfd_start', 'm106a_vfd_stop', 'pit_106a_pressure', 'fit_106a_flow_rate', 'm106b_vfd_active', 'm106b_vfd_faulted', 'm106b_vfd_frequency', 'm106b_vfd_start', 'm106b_vfd_stop', 'pit_106b_pressure', 'fit_106b_flow_rate', 'pit_106c_pressure', 'pit_106d_pressure', 'sdv106_open', 'sdv106_closed', 'bp_3a_auto', 'bp_3a_hand', 'bp_3a_run_cmd', 'bp_3a_run', 'bp_3a_fault', 'bp_3b_auto', 'bp_3b_hand', 'bp_3b_run_cmd', 'bp_3b_run', 'bp_3b_fault', 'pit_107a_pressure', 'fit_107a_flow_rate', 'pit_107b_pressure', 'fcv_001_valve', 'fit_107b_flow_rate', 'pit_107d_pressure', 'fcv_002_valve', 'pit_107c_pressure', 'pit_108a_pressure', 'pit_108b_pressure', 'dpi_108a_pressure', 'pit_108c_pressure', 'pit_108d_pressure', 'pdt_108b_pressure', 'pit_108e_pressure', 'pit_108f_pressure', 'pdt_108c_pressure', 'pit_108_pressure', 'pdt_108a_hi_alm', 'pdt_108a_hihi_alm', 'pdt_108b_hi_alm', 'pdt_108b_hihi_alm', 'pdt_108c_hi_alm', 'pdt_108c_hihi_alm', 'ait_102c_ph', 'ait_102d_oil_in_water', 'fit_102_flow_rate', 'lit_112a_h2o2_level', 'lit_112b_nahso3_level', 'fis_112_h2o2_popoff', 'fit_112a_h2o2_flow_rate', 'fit_112b_nahso3_flow_rate', 'at_109d_o2_in_water', 'fit_100_hi_alm', 'fit_100_hihi_alm', 'fit_100_lo_alm', 'fit_111_flow_rate', 'pit_110_pressure', 'lit_170_level', 'lit_200_level', 'lit_101_level', 'li_103D_level_alm', 'lsh_120_hihi_alm', 'pit_050_pressure', 'pit_065_pressure', 'pdi_065_pressure', 'fit_104_n2_rate', 'p100_auto', 'p100_hand', 'sales_recirculate_sw', 'fit_109a_flow_rate', 'pit_111a_n2', 'pit_111b_n2', 'pit_111c_n2', 'ct_200_current', 'sdv_101a', 'xy_100_run', 'skim_total_barrels', 'dpi_108b_pressure', 'chemical_pump_01_run_status', 'chemical_pump_01_rate_offset', 'spt_pid_h2o2_chemical_rate', 'spt_chemical_manual_rate', 'chemical_pump_auto', 'esd_exists', 'n2_purity', 'n2_outlet_flow_rate', 'n2_outlet_temp', 'n2_inlet_pressure', 'compressor_controller_temp', 'compressor_ambient_temp', 'compressor_outlet_temp', 'compressor_outlet_pressure', 'n2_outlet_pressure', 'fit_109b_water_job', 'fit_109b_water_last_month', 'fit_109b_water_month', 'fit_109b_water_lifetime', 'fit_109b_water_today', 'fit_109b_water_yesterday', 'fit_100_water_job', 'fit_100_water_last_month', 'fit_100_water_month', 'fit_100_water_lifetime', 'fit_100_water_today', 'fit_100_water_yesterday', 'h2o2_chemical_rate', 'rmt_sd_alm', 'pnl_esd_alm', 'pit_111c_hihi_alm', 'pit_111b_hihi_alm', 'pit_111a_hihi_alm', 'pit_110_hihi_alm', 'pit_108g_hihi_alm', 'pit_108c_hihi_alm', 'pit_108b_hihi_alm', 'pit_108a_hihi_alm', 'pit_107b_lolo_alm', 'pit_107a_lolo_alm', 'pit_106b_hihi_alm', 'pit_106a_hihi_alm', 'pit_101b_transmitter_alm', 'pit_101b_hihi_alm', 'pit_101a_transmitter_alm', 'pit_101a_hihi_alm', 'pit_101a_hi_alm', 'pit_100_hihi_alm', 'pit_065_hihi_alm', 'pit_050_hihi_alm', 'pdi_065_lolo_alm', 'pdi_065_lo_alm', 'pdi_065_hihi_alm', 'm106b_vfd_faulted_alm', 'm106a_vfd_faulted_alm', 'lit_200_hihi_alm', 'lit_170_hihi_alm', 'fit_107b_lolo_alm', 'fit_107a_lolo_alm', 'fit_106b_hihi_alm', 'fit_106a_hihi_alm', 'fit_004_hihi_alm', 'bp_3b_run_fail_alm', 'bp_3a_run_fail_alm', 'ait_114c_hihi_alm', 'ait_114b_hihi_alm', 'ait_114a_hihi_alm', 'ac_volt', 'bc_volt', 'ab_volt', 'psd_alm', 'ait_114a_lolo_alm', 'ait_114a_lo_alm', 'ait_114r_lolo_alm', 'ait_114r_lo_alm', 'ait_114z_lo_alm', 'ait_114z_lolo_alm', 'ait_114x_lo_alm', 'ait_114x_lolo_alm', 'ait_114c_lolo_alm', 'ait_114c_lo_alm', 'ait_114l_lolo_alm', 'ait_114l_lo_alm', 'lit_116b_hihi_alm', 'lit_116b_hi_alm', 'lit_116a_hihi_alm', 'lit_116a_hi_alm']
#Create a Sheet for each Device
for device in telemetry.keys():
df = getDataFrame(telemetry[device], ignore_keys, time)
return df
if __name__ == '__main__':
app.run(debug=True)

View File

@@ -3,3 +3,5 @@ requests
weasyprint
plotly
kaleido
pandas==2.2.2
tb-rest-client==3.7.0

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -14,13 +14,13 @@
</tr>
</thead>
<tbody>
{% for index,row in table_data.iterrows() %}
<!-- {% for index,row in table_data.iterrows() %}
<tr>
<td>{{ row['date'] }}</td>
<td>{{ row['GOOG'] }}</td>
<td>{{ row['AAPL'] }}</td>
<td>{{ row['Name'] }}</td>
<td>{{ row['INLET TURBIDITY'] }}</td>
<td>{{ row['INLET FLOW RATE'] }}</td>
</tr>
{% endfor %}
{% endfor %} -->
</tbody>
</table>