diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef9da07..27ee442 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,6 @@ jobs: with: fetch-depth: 0 - # This enables task distribution via Nx Cloud - # Run this command as early as possible, before dependencies are installed - # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun - - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build" - # Cache node_modules - uses: actions/setup-node@v4 with: @@ -33,6 +28,6 @@ jobs: - uses: nrwl/nx-set-shas@v4 # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud - # - run: npx nx-cloud record -- echo Hello World + # - run: npx nx record -- echo Hello World # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected - run: npx nx affected -t lint test build diff --git a/.gitignore b/.gitignore index 4f4d87b..01d8498 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ Thumbs.db .nx/cache .nx/workspace-data +.env diff --git a/.prettierrc b/.prettierrc index e87976a..602f24d 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { + "printWidth": 100, "semi": false, "singleQuote": true, "tabWidth": 4, diff --git a/apps/mqtt-gateway/.env.example b/apps/mqtt-gateway/.env.example new file mode 100644 index 0000000..f22e77e --- /dev/null +++ b/apps/mqtt-gateway/.env.example @@ -0,0 +1,6 @@ +AWEKAS_ID=your-awkas-id +AWEKAS_PASSWORD=your-awekas-password +MPLABS_ID=your-local-id +MPLABS_PASSWORD=your-local-password +MQTT_BROKER_URL=mqtt://127.0.0.1:1883 +MQTT_TOPIC=weather diff --git a/apps/mqtt-gateway/Dockerfile b/apps/mqtt-gateway/Dockerfile index 57e9d7f..d4de5a0 100644 --- a/apps/mqtt-gateway/Dockerfile +++ b/apps/mqtt-gateway/Dockerfile @@ -12,7 +12,7 @@ ENV PORT=3000 WORKDIR /app RUN addgroup --system mqtt-gateway && \ - adduser --system -G mqtt-gateway mqtt-gateway + adduser --system -G mqtt-gateway mqtt-gateway COPY dist/apps/mqtt-gateway mqtt-gateway/ RUN chown -R mqtt-gateway:mqtt-gateway . diff --git a/apps/mqtt-gateway/compose.yml b/apps/mqtt-gateway/compose.yml new file mode 100644 index 0000000..0493be0 --- /dev/null +++ b/apps/mqtt-gateway/compose.yml @@ -0,0 +1,30 @@ +services: + watchtower: + image: containrrr/watchtower + command: + - "--label-enable" + - "--interval" + - "30" + - "--rolling-restart" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /etc/localtime:/etc/localtime:ro + + mqtt-gateway: + image: fduerrwald/awekas:latest + labels: + - "traefik.enable=true" + - "traefik.http.routers.mqtt-gateway.rule=Host(`ws.mplabshome.net`)" + - "traefik.http.routers.mqtt-gateway.entrypoints=web" + - "com.centurylinklabs.watchtower.enable=true" + environment: + - AWEKAS_ID=${AWEKAS_ID} + - AWEKAS_PASSWORD=${AWEKAS_PASSWORD} + - MPLABS_ID=${MPLABS_ID} + - MPLABS_PASSWORD=${MPLABS_PASSWORD} + - MQTT_BROKER_URL=${MQTT_BROKER_URL:-mqtt://127.0.0.1:1883} + - MQTT_TOPIC=${MQTT_TOPIC:-weather} + deploy: + mode: replicated + replicas: 3 + restart: always diff --git a/apps/mqtt-gateway/src/chatgpt.ts b/apps/mqtt-gateway/src/chatgpt.ts deleted file mode 100644 index 15e3cd9..0000000 --- a/apps/mqtt-gateway/src/chatgpt.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { ParsedQs } from 'qs' -import express, { Request, Response } from 'express' -import morgan from 'morgan' - -const app = express() -const port = 3000 - -// Utility function to check required fields -const checkRequiredFields = (params: ParsedQs): boolean => { - const requiredFields = ['ID', 'PASSWORD', 'dateutc', 'action'] - for (const field of requiredFields) { - if (!params[field]) { - return false - } - } - return true -} - -app.use(morgan(':method :url :status :res[content-length] - :response-time ms')) - -// Route to handle weather station updates -app.get( - '/weatherstation/updateweatherstation.php', - (req: Request, res: Response) => { - const params = req.query - - // Check if required fields are present - if (!checkRequiredFields(params)) { - return res - .status(400) - .send( - 'RapidFire Server

usage
Required fields missing: ID, PASSWORD, action, and dateutc' - ) - } - - const { ID, PASSWORD, action, dateutc } = params as Record - - // Validate action and dateutc - if (action !== 'updateraw') { - return res.status(400).send('Invalid action') - } - - if (dateutc !== 'now' && isNaN(Date.parse(dateutc))) { - return res.status(400).send('Invalid dateutc format') - } - - // Validate ID and PASSWORD (this is where you would verify against your database) - // For now, we'll mock this validation - if (ID !== 'KCASANFR5' || PASSWORD !== 'XXXXXX') { - return res - .status(401) - .send('INVALIDPASSWORDID|Password and/or id are incorrect') - } - - // Success response - res.status(200).send('success') - - // Log the incoming data - console.log('Received weather data:', params) - } -) - -// Start the server -app.listen(port, () => { - console.log(`Server is running at http://localhost:${port}`) -}) diff --git a/apps/mqtt-gateway/src/main.ts b/apps/mqtt-gateway/src/main.ts index 5cdd3a5..3624239 100644 --- a/apps/mqtt-gateway/src/main.ts +++ b/apps/mqtt-gateway/src/main.ts @@ -1,43 +1,57 @@ -import express from 'express'; -import { checkSchema, Schema } from 'express-validator' +import dotenv from 'dotenv' +import express, { NextFunction, Request, Response } from 'express' +import morgan from 'morgan' +import mqtt from 'mqtt' +import checkRequiredFieldsMiddleware from './middleware/checkRequiredFields' +import validateParamsMiddleware from './middleware/validateParams' +import forwardDataMiddleware from './middleware/forwardData' +import { WSQuery } from './types' +import sendToMqtt from './middleware/sentToMqtt' -const host = process.env.HOST ?? 'localhost'; -const port = process.env.PORT ? Number(process.env.PORT) : 3000; +dotenv.config() -const app = express(); +const { AWEKAS_ID, AWEKAS_PASSWORD, MPLABS_ID, MPLABS_PASSWORD, MQTT_BROKER_URL, MQTT_TOPIC } = process.env -const FtoC: (f: number) => number = (f) => (f - 32) * 5 / 9 -const inHgTohpa: (inHg: number) => number = (inHg) => inHg * 33.86389 -const mphToKmh: (mph: number) => number = (mph) => mph * 1.609344 -const mphToMs: (mph: number) => number = (mph) => mph * 0.44704 +const app = express() +const port = 3000 -type UpdateweatherstationQuery = Record & { - action: 'updateraw' - ID: string - KEY: string -} +// MQTT Client setup +const mqttClient = mqtt.connect(MQTT_BROKER_URL) // Replace with your MQTT server URL and port -app.get('/weatherstation/updateweatherstation.php', (req, res) => { - const query = req.query as Record +mqttClient.on('connect', () => { + console.log('Connected to MQTT broker') +}) - // Validate only the required fields for now - const { action, ID, KEY } = query +mqttClient.on('error', (err) => { + console.error('MQTT connection error:', err) +}) - const timestamp = new Date().getTime() - const pressure_inhg = parseFloat(query.baromin) - const pressure_hpa = inHgTohpa(parseFloat(query.baromin)) - const temperature_f = parseFloat(query.tempf) - const temperature_c = FtoC(parseFloat(query.tempf)) - const dew_point_f = parseFloat(query.dewptf) - const dew_point_c = FtoC(parseFloat(query.dewptf)) - const humidity = parseFloat(query.humidity) - const wind_speed_mph = parseFloat(query.windspeedmph) - const wind_speed_mph = mphToKmh(parseFloat(query.windspeedmph)) - const wind_speed_ms = mphToMs(parseFloat(query.windspeedmph)) +app.use(morgan(':method :url :status :res[content-length] - :response-time ms')) - res.send('OK') -}); +// Routes +app.get('/', (req: Request, res: Response) => { + res.send('AWEKAS Data Server URL') +}) -app.listen(port, host, () => { - console.log(`[ ready ] http://${host}:${port}`); -}); +// Route to handle weather station updates +app.get( + '/weatherstation/updateweatherstation.php', + checkRequiredFieldsMiddleware, + validateParamsMiddleware(MPLABS_ID, MPLABS_PASSWORD), + forwardDataMiddleware(AWEKAS_ID, AWEKAS_PASSWORD), + sendToMqtt(mqttClient, MQTT_TOPIC), + (req: Request, res: Response) => { + const params = req.query as WSQuery + + // Success response + res.status(200).send(' - airp locked - Transfer OK') + + // Log the incoming data + console.log('Received weather data:', params) + } +) + +// Start the server +app.listen(port, () => { + console.log(`Server is running at http://localhost:${port}`) +}) diff --git a/apps/mqtt-gateway/src/middleware/checkRequiredFields.ts b/apps/mqtt-gateway/src/middleware/checkRequiredFields.ts new file mode 100644 index 0000000..3d608b5 --- /dev/null +++ b/apps/mqtt-gateway/src/middleware/checkRequiredFields.ts @@ -0,0 +1,17 @@ +import { NextFunction, Request, Response } from "express" +import { WSQuery } from "../types" + +// Utility middleware to check required fields +const checkRequiredFieldsMiddleware = (req: Request, res: Response, next: NextFunction) => { + const params = req.query as WSQuery + const requiredFields = ['ID', 'PASSWORD', 'dateutc', 'action'] + if (!requiredFields.every((field) => Boolean(params[field]))) { + res.status(400).send( + `AWEKAS Data Server URL
Required fields missing: ID, PASSWORD, action, and dateutc` + ) + } else { + next() + } +} + +export default checkRequiredFieldsMiddleware diff --git a/apps/mqtt-gateway/src/middleware/forwardData.ts b/apps/mqtt-gateway/src/middleware/forwardData.ts new file mode 100644 index 0000000..8bfe8a3 --- /dev/null +++ b/apps/mqtt-gateway/src/middleware/forwardData.ts @@ -0,0 +1,35 @@ +import { NextFunction, Request, Response } from "express" +import { WSQuery } from "../types" + +// Forward received data to AWEKAS +const forwardDataMiddleware = (AWEKAS_ID: string, AWEKAS_PASSWORD: string) => async (req: Request, res: Response, next: NextFunction) => { + const awekasServerUrl = 'http://ws.awekas.at/weatherstation/updateweatherstation.php' + const queryParams = new URLSearchParams(req.query as WSQuery) + queryParams.set('ID', AWEKAS_ID) + queryParams.set('PASSWORD', AWEKAS_PASSWORD) + const awekasUrlWithParams = `${awekasServerUrl}?${queryParams.toString()}` + + try { + // Forward the request using the native fetch API + const awekasResponse = await fetch(awekasUrlWithParams, { + method: 'GET', + headers: { + Host: 'ws.awekas.at', // Override the host header to match the expected hostname + }, + }) + + // If the forward request is successful, log and continue to the next handler + const awekasData = await awekasResponse.text() + console.log('Response from Awekas:', awekasData) + + // Move to the next middleware or route handler + next() + } catch (error) { + console.error('Error forwarding request to Awekas:', error) + + // If there's an error, respond with a 502 Bad Gateway + res.status(502).send('Error forwarding request to Awekas server') + } +} + +export default forwardDataMiddleware diff --git a/apps/mqtt-gateway/src/middleware/sentToMqtt.ts b/apps/mqtt-gateway/src/middleware/sentToMqtt.ts new file mode 100644 index 0000000..6d37b35 --- /dev/null +++ b/apps/mqtt-gateway/src/middleware/sentToMqtt.ts @@ -0,0 +1,36 @@ +import { NextFunction, Request, Response } from 'express' +import { WSQuery } from '../types' +import { MqttClient } from 'mqtt/*' +import transformData from '../utils/transformData' +import { omit } from '../utils/object' + +// Middleware to send data to MQTT server +const sendToMqtt = + (mqttClient: MqttClient, MQTT_TOPIC: string) => + (req: Request, res: Response, next: NextFunction) => { + const { ID, ...params } = omit( + req.query as WSQuery, + 'PASSWORD', + 'action', + 'realtime', + 'rtfreq' + ) + + // Format the data to be sent to MQTT + const data = transformData(params) + + // Send the data to the MQTT server + for (const [key, value] of Object.entries(data)) { + const topic = `${MQTT_TOPIC}/${ID}/${key}` + mqttClient.publish(topic, value.toString(), (err) => { + if (err) { + console.error('Failed to publish to MQTT:', err) + } + }) + } + + // Proceed to the next middleware + next() + } + +export default sendToMqtt diff --git a/apps/mqtt-gateway/src/middleware/validateParams.ts b/apps/mqtt-gateway/src/middleware/validateParams.ts new file mode 100644 index 0000000..1e6adc3 --- /dev/null +++ b/apps/mqtt-gateway/src/middleware/validateParams.ts @@ -0,0 +1,24 @@ +import { NextFunction, Request, Response } from "express" +import { WSQuery } from "../types" + +// Validate input parameters +const validateParamsMiddleware = (MPLABS_ID: string, MPLABS_PASSWORD: string) => (req: Request, res: Response, next: NextFunction) => { + const { ID, PASSWORD, action, dateutc } = req.query as WSQuery + + // Validate action and dateutc + if (action !== 'updateraww') { + res.status(400).send('Invalid action') + } else if (dateutc !== 'now' && isNaN(Date.parse(dateutc))) { + res.status(400).send('Invalid dateutc format') + } + + // Validate ID and PASSWORD (this is where you would verify against your database) + // For now, we'll mock this validation + else if (ID !== MPLABS_ID || PASSWORD !== MPLABS_PASSWORD) { + return res.status(401).send('INVALIDPASSWORDID|Password and/or id are incorrect') + } else { + next() + } +} + +export default validateParamsMiddleware diff --git a/apps/mqtt-gateway/src/types.ts b/apps/mqtt-gateway/src/types.ts new file mode 100644 index 0000000..9e3e462 --- /dev/null +++ b/apps/mqtt-gateway/src/types.ts @@ -0,0 +1,21 @@ +export type WSKeys = + | 'ID' + | 'PASSWORD' + | 'action' + | 'realtime' + | 'rtfreq' + | 'dateutc' + | 'baromin' + | 'tempf' + | 'dewptf' + | 'humidity' + | 'windspeedmph' + | 'windgustmph' + | 'winddir' + | 'rainin' + | 'dailyrainin' + | 'solarradiation' + | 'UV' + | 'indoortempf' + | 'indoorhumidity' +export type WSQuery = Record diff --git a/apps/mqtt-gateway/src/utils/object.ts b/apps/mqtt-gateway/src/utils/object.ts new file mode 100644 index 0000000..a4458da --- /dev/null +++ b/apps/mqtt-gateway/src/utils/object.ts @@ -0,0 +1,11 @@ +// This function, omit, takes an object and an array of property keys, returning a new object +// that excludes the specified properties. It ensures type safety by allowing only valid keys +// from the original object to be omitted, without mutating the original data. +export function omit(obj: T, ...props: K[]): Omit { + return (Object.keys(obj) as K[]).reduce((acc, key) => { + if (!props.includes(key)) { + acc[key] = obj[key] + } + return acc + }, {} as T) +} diff --git a/apps/mqtt-gateway/src/utils/transformData.ts b/apps/mqtt-gateway/src/utils/transformData.ts new file mode 100644 index 0000000..6c7e1a5 --- /dev/null +++ b/apps/mqtt-gateway/src/utils/transformData.ts @@ -0,0 +1,62 @@ +import { WSQuery } from '../types' + +// Returns the current UTC time in the format +// YYYY-MM-DD HH:MM:SS +const getCurrentTimeString = () => { + const now = new Date() + const formatter = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZone: 'UTC', + }) + return formatter.format(now).replace(',', '') +} + +// Convert miles per hour to kilometers per hour +const mphToKph = (mph: string) => (parseFloat(mph) * 1.60934).toFixed(1) + +// Convert Fahreneheit to Celsius +const fToC = (f: string) => (((parseFloat(f) - 32) * 5) / 9).toFixed(1) + +// Convert inches Hg to hectopascal +const inHgTohPa = (inHg: string) => (parseFloat(inHg) * 33.8639).toFixed(1) + +// Convert inches to mm +const inchToCm = (inch: string) => (parseFloat(inch) * 25.4).toFixed(1) + +// Calculate the wet bulb temperature +// +const wetbulbtemperature = (tempf: string, humidity: string) => { + const T = ((parseFloat(tempf) - 32) * 5) / 9 + const RH = parseFloat(humidity) + return ( + T * Math.atan(0.151977 * Math.sqrt(RH + 8.313659)) + + Math.atan(T + RH) - + Math.atan(RH - 1.676331) + + 0.00391838 * Math.pow(RH, 1.5) * Math.atan(0.023101 * RH) - + 4.686035 + ).toFixed(2) +} + +const transformData = ( + params: Omit +) => ({ + ...params, + dateutc: params.dateutc === 'now' ? getCurrentTimeString() : params.dateutc, + windspeedkph: mphToKph(params.windspeedmph), + windgustkph: mphToKph(params.windgustmph), + tempc: fToC(params.tempf), + dewptc: fToC(params.dewptf), + baromhpa: inHgTohPa(params.baromin), + rainmm: inchToCm(params.rainin), + dailyrainmm: inchToCm(params.dailyrainin), + indoortempc: fToC(params.indoortempf), + wetbulbtemperature: wetbulbtemperature(params.tempf, params.humidity), +}) + +export default transformData diff --git a/package-lock.json b/package-lock.json index 0636d20..784c9ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "dependencies": { "@types/morgan": "^1.9.9", "axios": "^1.6.0", + "dotenv": "^16.4.5", "express": "~4.18.1", "express-validator": "^7.2.0", "morgan": "^1.10.0", + "mqtt": "^5.10.1", "tslib": "^2.3.0" }, "devDependencies": { @@ -1917,7 +1919,6 @@ "version": "7.25.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3943,6 +3944,20 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/readable-stream": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz", + "integrity": "sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -3970,6 +3985,14 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -4228,6 +4251,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -4620,7 +4654,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -4800,8 +4833,7 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/bytes": { "version": "3.1.2", @@ -5024,12 +5056,31 @@ "node": ">= 0.8" } }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -5148,7 +5199,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -5310,7 +5360,6 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "dev": true, "engines": { "node": ">=12" }, @@ -5792,6 +5841,22 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -5952,6 +6017,18 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.1.0" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -6467,6 +6544,11 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -6542,7 +6624,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -7517,6 +7598,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7866,7 +7956,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7910,6 +7999,139 @@ "node": ">= 0.8" } }, + "node_modules/mqtt": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.1.tgz", + "integrity": "sha512-hXCOki8sANoQ7w+2OzJzg6qMBxTtrH9RlnVNV8panLZgnl+Gh0J/t4k6r8Az8+C7y3KAcyXtn0mmLixyUom8Sw==", + "dependencies": { + "@types/readable-stream": "^4.0.5", + "@types/ws": "^8.5.9", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.3.4", + "help-me": "^5.0.0", + "lru-cache": "^10.0.1", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.0", + "number-allocator": "^1.0.14", + "readable-stream": "^4.4.2", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^4.2.0", + "worker-timers": "^7.1.4", + "ws": "^8.17.1" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.0.tgz", + "integrity": "sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt-packet/node_modules/bl": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.16.tgz", + "integrity": "sha512-V/kz+z2Mx5/6qDfRCilmrukUXcXuCoXKg3/3hDvzKKoSUx8CJKudfIoT29XZc3UE9xBvxs5qictiHdprwtteEg==", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/mqtt-packet/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/mqtt-packet/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/mqtt/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/mqtt/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/mqtt/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7983,6 +8205,15 @@ "node": ">=8" } }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, "node_modules/nx": { "version": "19.7.3", "resolved": "https://registry.npmjs.org/nx/-/nx-19.7.3.tgz", @@ -8405,6 +8636,19 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8526,7 +8770,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8557,8 +8800,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -8607,6 +8849,11 @@ "jsesc": "bin/jsesc" } }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8686,6 +8933,11 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -8910,6 +9162,14 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -8949,7 +9209,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -9348,6 +9607,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", @@ -9460,8 +9724,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utils-merge": { "version": "1.0.1", @@ -9558,6 +9821,37 @@ "node": ">=0.10.0" } }, + "node_modules/worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9594,6 +9888,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 224f832..4f5b511 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,11 @@ "dependencies": { "@types/morgan": "^1.9.9", "axios": "^1.6.0", + "dotenv": "^16.4.5", "express": "~4.18.1", "express-validator": "^7.2.0", "morgan": "^1.10.0", + "mqtt": "^5.10.1", "tslib": "^2.3.0" }, "devDependencies": {