From ee8d386cd82b74ab5fde6c5760b38f4d543dece1 Mon Sep 17 00:00:00 2001 From: Nico Melone Date: Wed, 4 Jun 2025 18:01:43 -0500 Subject: [PATCH] added more functions --- .../__pycache__/thingsboard.cpython-313.pyc | Bin 5762 -> 9592 bytes LLM Integration/mcp_functions.py | 114 ++++++++++++++++++ LLM Integration/thingsboard.py | 101 +++++++++++++++- 3 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 LLM Integration/mcp_functions.py diff --git a/LLM Integration/__pycache__/thingsboard.cpython-313.pyc b/LLM Integration/__pycache__/thingsboard.cpython-313.pyc index eec72a71e0374fb457ebb608eb4f423f23648b2e..70e5f538a52bd50ddcea62807eedaa6258c826ce 100644 GIT binary patch delta 4752 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 biz+MC9%sk@)Ree;<;b(RBc?~Ebt>>55PAUs diff --git a/LLM Integration/mcp_functions.py b/LLM Integration/mcp_functions.py new file mode 100644 index 0000000..6f4cb67 --- /dev/null +++ b/LLM Integration/mcp_functions.py @@ -0,0 +1,114 @@ +from model import * +from fastapi import FastAPI, HTTPException, Body +from fastapi.middleware.cors import CORSMiddleware +from typing import Literal +import requests, json +from datetime import datetime as dt +from datetime import timedelta as td +app = FastAPI( + title="Thingsboard API", + version="1.0.0", + description="Provides secure access to Thingsboard API", +) + + +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +def getJWT(): + response = requests.post(url_base + 'auth/login', headers=headers,data=credentials) + token = response.json() + headers["X-Authorization"] = "Bearer " + token['token'] + +@app.get("/customers", response_model=List[Customer]) +def getCustomers(page: int = 0, pageSize: int = 10): + """ + Returns a list of customers from Thingsboard API. + """ + response = requests.get( + url_base + f"customers?page={page}&pageSize={pageSize}", headers=headers) + #print(json.dumps(response.json(), indent=4)) + customers = [] + customers = response.json()['data'] + while(response.json()["hasNext"]): + page += 1 + response = requests.get(url_base + f"customers?page={page}&pageSize={pageSize}", headers=headers) + customers += response.json()['data'] + #print(customers) + return customers + +@app.get("/customerNames", response_model=List[str]) +def getCustomerNames(page=0, pageSize=10): + """ + Returns a list of customer names from Thingsboard API. + """ + customers = getCustomers(page=0, pageSize=10) + names = [] + for c in customers: + names.append(c['name']) + #print(names) + return names + + +@app.get("/telemetry", response_model=dict) +def getTelemetry(startTs, endTs, keys, eType, deviceId): + """ + Returns a list of telemetry data from Thingsboard API. + parameters: + startTs: Timestamp of the start time for the data in milliseconds. + endTs: Timestamp of the end time for the data in milliseconds. + keys: List of attribute keys to retrieve (e.g., temperature,humidity,etc.). + eType: Type of entity (e.g., 'ASSET', 'DEVICE'). + deviceId: ID of the device as a UUID + """ + if(not keys): + keys = ["temperature", "humidity"] + if(not startTs): + #set startTs to time from 1 hour ago and put it in timestamp format with milliseconds + startTs = dt.timestamp(dt.now() - td(hours=1)) * 1000 + if(not endTs): + #set endTs to time from now and put it in timestamp format with milliseconds + endTs = dt.timestamp(dt.now()) * 1000 + telemetry = requests.get( + url_base + f"plugins/telemetry/{eType}/{deviceId}/values/timeseries?startTs={startTs}&endTs={endTs}&keys={keys}", headers=headers) + print(telemetry.json()) + return telemetry.json() + +@app.get("/deviceIdByName", response_model=dict) +def getDeviceByName(textSearch: str, deviceType: str = None, sortProperty: str = None, sortOrder: str = "ASC",page: int = 0, pageSize: int = 10): + """ + Returns a dictionary with the device ID (as a UUID) of a device with a given name in the textSearch param + parameters: + deviceType: optional parameter to specify the device type for search + textSearch: the string or substring of the name of the device being searched for + sortProperty: optional parameter to specify the property to sort by can be one of the following [createdTime, name, deviceProfileName, label, customerTitle] + sortOrder: optional parameter to specify is ascending or descending order [ASC, DESC] + page: the page of results to acquire default is set to 0 to get the first page + pageSize: the number of results per page + """ + response = requests.get(url_base + f"user/devices?page={page}&pageSize={pageSize}&textSearch={textSearch}", headers=headers) + #print(response.url) + #print(response.request) + #print(json.dumps(response.json(),indent=4)) + devices = response.json()["data"] + device = {textSearch: devices[0]["id"]["id"]} + return device +headers = {"Content-Type": "application/json", + "Accept": "application/json"} + +username = "example@example.com" +password = "super-secure-password" +domain = "thingsboard.cloud" +credentials = json.dumps( + {"username": f"{username}", "password": f"{password}"}) +url_base = f"https://{domain}/api/" +getJWT() +#print(headers) diff --git a/LLM Integration/thingsboard.py b/LLM Integration/thingsboard.py index a26a937..285a90f 100644 --- a/LLM Integration/thingsboard.py +++ b/LLM Integration/thingsboard.py @@ -5,6 +5,7 @@ from typing import Literal import requests, json from datetime import datetime as dt from datetime import timedelta as td +from datetime import timezone as tz app = FastAPI( title="Thingsboard API", version="1.0.0", @@ -69,19 +70,46 @@ def getTelemetry(startTs, endTs, keys, eType, deviceId): eType: Type of entity (e.g., 'ASSET', 'DEVICE'). deviceId: ID of the device as a UUID """ - if(not keys): + if not keys: keys = ["temperature", "humidity"] - if(not startTs): + if not startTs: #set startTs to time from 1 hour ago and put it in timestamp format with milliseconds startTs = dt.timestamp(dt.now() - td(hours=1)) * 1000 - if(not endTs): + if not endTs: #set endTs to time from now and put it in timestamp format with milliseconds endTs = dt.timestamp(dt.now()) * 1000 telemetry = requests.get( url_base + f"plugins/telemetry/{eType}/{deviceId}/values/timeseries?startTs={startTs}&endTs={endTs}&keys={keys}", headers=headers) - print(telemetry.json()) + #print(telemetry.json()) return telemetry.json() + +@app.get("/attributes", response_model=dict) +def getAttributes(device_id: str, scope: str = "SERVER_SCOPE"): + """ + Retrieves attributes for a given device from ThingsBoard. + + Parameters: + device_id (str): UUID of the device. + scope (str): Attribute scope to retrieve. Options: + - CLIENT_SCOPE + - SERVER_SCOPE (default) + - SHARED_SCOPE + + Returns: + dict: Attributes for the device. + """ + url = url_base + \ + f"plugins/telemetry/DEVICE/{device_id}/values/attributes?scope={scope}" + response = requests.get(url, headers=headers) + + if response.status_code != 200: + raise HTTPException( + status_code=response.status_code, detail=response.text) + + return response.json() + + @app.get("/deviceIdByName", response_model=dict) def getDeviceByName(textSearch: str, deviceType: str = None, sortProperty: str = None, sortOrder: str = "ASC",page: int = 0, pageSize: int = 10): """ @@ -101,8 +129,73 @@ def getDeviceByName(textSearch: str, deviceType: str = None, sortProperty: str = devices = response.json()["data"] device = {textSearch: devices[0]["id"]["id"]} return device + + +def getDevicesByCustomerId(customer_id): + response = requests.get( + url_base + f"customer/{customer_id}/devices?pageSize=4&page=0", headers=headers + ) + #print(response.json()) + return response.json().get('data', []) + + +def getLastTelemetryTimestamp(device_id, keys=None): + end_ts = int(dt.now(tz.utc).timestamp() * 1000) + start_ts = int((dt.now(tz.utc) - td(hours=1)).timestamp() * 1000) + data = getTelemetry(startTs=start_ts, endTs=end_ts, + keys=keys, eType="DEVICE", deviceId=device_id) + timestamps = [] + for key_data in data.values(): + for entry in key_data: + timestamps.append(entry.get('ts', 0)) + return max(timestamps, default=None) + +def summarizeCustomerDevices(): + summary = {} + customers = getCustomers() + #print(customers) + now_ms = int(dt.now(tz.utc).timestamp() * 1000) + stale_threshold = now_ms - (30 * 60 * 1000) # 30 minutes ago + + for customer in customers: + customer_id = customer["id"]["id"] + devices = getDevicesByCustomerId(customer_id) + + active = 0 + inactive = 0 + stale_devices = [] + for device in devices: + #print(device) + isActive = False + last_ts = 0 + for key in getAttributes(device_id=device["id"]["id"]): + if key.get("key") == "active": + isActive = key.get("value") + if key.get("key") == "latestReportTime": + last_ts = key.get("value") + if isActive: + active += 1 + if last_ts is None or last_ts < stale_threshold: + stale_devices.append(device["name"]) + else: + inactive += 1 + + summary[customer["name"]] = { + "active_devices": active, + "inactive_devices": inactive, + "stale_active_devices": stale_devices + } + + return summary + + +@app.get("/summary") +def getDeviceSummary(): + return summarizeCustomerDevices() + headers = {"Content-Type": "application/json", "Accept": "application/json"} + username = "nmelone@henry-pump.com" password = "gzU6$26v42mU%3jDzTJf" domain = "thingsboard.cloud"