3 Commit-ok 7d18eb165e ... 3ee83966a5

Szerző SHA1 Üzenet Dátum
  Jason Tarka 3ee83966a5 Add graph for max delay during a time period 2 éve
  Jason Tarka a85e8fce05 Updating packages + small tweaks 2 éve
  Jason Tarka 39e94de20c package-lock.json 2 éve

+ 2 - 0
Arduino/outdoor-weather-station/03_log_data.ino

@@ -3,6 +3,7 @@
 
 #define PORT 443
 
+// Connect to example.com as a test method.
 void getSomething() {
 	showProgress(PROGRESS_GREEN);
 	String content = String("GET / HTTP/1.1\r\n")
@@ -37,6 +38,7 @@ void getSomething() {
 	debug("===== Done =====");
 }
 
+// Upload data to RTDB.
 boolean uploadData(
 	float temperature,
 	float pressure,

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "weather-station",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {}
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 425 - 334
web-view/package-lock.json


+ 14 - 14
web-view/package.json

@@ -11,11 +11,13 @@
     "build:js": "webpack --mode=production",
     "watch:js": "webpack --mode=development --watch",
     "watch:html": "npm-watch",
-    "deploy": "npm run build && scp -r build/* storm:/mnt2/nginx-data/weather/"
+    "deploy": "npm run clean && npm run build && scp -r build/* storm:/mnt2/nginx-storage/weather/"
   },
   "watch": {
     "build:html": {
-      "patterns": ["src"],
+      "patterns": [
+        "src"
+      ],
       "extensions": "html",
       "runOnChangeOnly": false
     }
@@ -24,21 +26,19 @@
   "author": "Jason Tarka <git@tarka.ca>",
   "license": "",
   "dependencies": {
-    "@types/firebase": "^3.2.1",
-    "chart.js": "^3.7.0",
-    "firebase": "^9.6.1",
-    "svg-gauge": "^1.0.6"
+    "chart.js": "^4.2.1",
+    "firebase": "^9.17.2",
+    "svg-gauge": "^1.0.7"
   },
   "devDependencies": {
-    "@types/mocha": "^9.0.0",
-    "@types/should": "^13.0.0",
-    "mocha": "^8.4.0",
+    "@types/mocha": "^10.0.1",
+    "mocha": "^10.2.0",
     "npm-watch": "^0.11.0",
     "should": "^13.2.3",
-    "ts-loader": "^9.2.6",
-    "ts-mocha": "^8.0.0",
-    "ts-node": "^10.4.0",
-    "typescript": "^4.5.4",
-    "webpack-cli": "^4.9.1"
+    "ts-loader": "^9.4.2",
+    "ts-mocha": "^10.0.0",
+    "ts-node": "^10.9.1",
+    "typescript": "^4.9.5",
+    "webpack-cli": "^5.0.1"
   }
 }

+ 21 - 5
web-view/src/js/cleanData.ts

@@ -1,18 +1,21 @@
 import {formatTime} from './utils';
 import {TimeSeries} from './types';
+import {WindowType} from "./consts";
 
 /**
  * Align data to a window, using the average value within that period.
  * For example, the value for 13:15:00 will be the average of values from
- * 13:00:00 to 13:14:59.999
+ * 13:07:30 to 13:22:30
  *
  * @param data The data to align, with a `time` field as a unix timestamp, and a
  *             data field specified in `dataField`.
  * @param period The number of minutes to group on.
+ * @param windowType The type of processing to do on the data.
  */
 export function alignToWindow(
 	data:TimeSeries[],
-	period:number = 15
+	period:number = 15,
+	windowType: WindowType = WindowType.AVERAGE
 ):TimeSeries[] {
 	const sorted = data.sort((a,b) => a.time - b.time);
 
@@ -46,12 +49,16 @@ export function alignToWindow(
 				// Push the average, or NaN if there are no elements
 				const w = {
 					time: windowTime,
-					value: average(currNums)
+					value: windowType == WindowType.MAX
+						? max(currNums)
+						: average(currNums)
 				};
 				windows.push(w);
 			}
 
-			// Reset for the next iteration
+			// Reset for the next iteration.
+			// Because the previous end time was at (period + 1/2), the
+			// loop will continue from (newPeriod - 1/2).
 			windowTime += periodMs;
 			windowEnd = windowTime + halfPeriodMs;
 			currNums = [];
@@ -64,7 +71,9 @@ export function alignToWindow(
 	if(currNums.length) {
 		const w = {
 			time: windowTime,
-			value: average(currNums)
+			value: windowType == WindowType.MAX
+				? max(currNums)
+				: average(currNums)
 		};
 		windows.push(w);
 	}
@@ -80,6 +89,13 @@ function average(nums:number[], decimalPlaces:number = 1):number {
 	return parseFloat((total / nums.length).toFixed(decimalPlaces));
 }
 
+function max(nums: number[], decimalPlaces:number = 1) {
+	if(!nums.length)
+		return NaN;
+
+	return Math.max(...nums).toFixed(decimalPlaces);
+}
+
 /**
  * Remove elements that are particularly "spiky" compared to the ones next to them.
  * Since the main concern is about the device heating up from multiple retries, a

+ 29 - 0
web-view/src/js/consts.ts

@@ -8,14 +8,24 @@ export const MAX_HOURS_AGO = 168;
  */
 export const RANGE_PADDING:number = 5;
 
+/**
+ * The type of alignment window to process.
+ */
+export enum WindowType {
+	AVERAGE,
+	MAX,
+};
+
 export type FieldInfo = {
 	key: string,
 	label: string,
 	unit: string,
 	colour: string,
 	canSpike: boolean,
+	windowType: WindowType,
 
 	// Used for gauges
+	excludeGauge: boolean,
 	minValue: number,
 	maxValue: number,
 	gaugeColours: any,
@@ -38,8 +48,10 @@ export const FIELDS: FieldInfo[] = [
 		unit: '°C',
 		colour: Colours.Red,
 		canSpike: true,
+		windowType: WindowType.AVERAGE,
 		minValue: -10,
 		maxValue: 30,
+		excludeGauge: false,
 		gaugeColours: {
 			'-10': Colours.PaleBlue,
 			0: Colours.Purple,
@@ -55,8 +67,10 @@ export const FIELDS: FieldInfo[] = [
 		unit: '%',
 		colour: Colours.Blue,
 		canSpike: false, // Affected by temperature, so can spike
+		windowType: WindowType.AVERAGE,
 		minValue: 0,
 		maxValue: 100,
+		excludeGauge: false,
 		gaugeColours: {
 			20: Colours.Orange,
 			30: Colours.Yellow,
@@ -70,8 +84,10 @@ export const FIELDS: FieldInfo[] = [
 		unit: 'hPa',
 		colour: Colours.Green,
 		canSpike: false,
+		windowType: WindowType.AVERAGE,
 		minValue: 960,
 		maxValue: 1030,
+		excludeGauge: false,
 		gaugeColours: {
 			990: Colours.Blue,
 			1000: Colours.PaleBlue,
@@ -84,8 +100,10 @@ export const FIELDS: FieldInfo[] = [
 		unit: '',
 		colour: Colours.Orange,
 		canSpike: false, // Does spike, but it doesn't matter
+		windowType: WindowType.AVERAGE,
 		minValue: 50,
 		maxValue: 8191,
+		excludeGauge: false,
 		gaugeColours: {
 			0: Colours.Blue,
 			1000: Colours.PaleBlue,
@@ -93,6 +111,17 @@ export const FIELDS: FieldInfo[] = [
 			5000: Colours.Yellow,
 			10000: Colours.Yellow
 		}
+	}, {
+		key: 'delay',
+		label: 'Max Delay',
+		unit: 'mins',
+		colour: Colours.Yellow,
+		canSpike: false,
+		windowType: WindowType.MAX,
+		minValue: 0,
+		maxValue: 10,
+		excludeGauge: true,
+		gaugeColours: null,
 	},
 ];
 

+ 9 - 7
web-view/src/js/graphs.ts

@@ -107,11 +107,13 @@ export function createGauges(
 		let fieldData = data[field.key];
 		let value = fieldData[fieldData.length - 1].value;
 
-		addGauge(
-			field,
-			value,
-			parentElement
-		);
+		if(!field.excludeGauge) {
+			addGauge(
+				field,
+				value,
+				parentElement
+			);
+		}
 	}
 }
 
@@ -130,8 +132,8 @@ function addGauge(
 		max: field.maxValue >= value ? field.maxValue : value + rangeIncrement,
 		min: field.minValue <= value ? field.minValue : value - rangeIncrement,
 		label: v => {
-			const sign = v > -0.5 && v < 0 ? '-' : ''; // Show minus zero
-			return `${sign}${v.toFixed(0)}${field.unit}`;
+			const decimalDigits = Math.abs(v) < 10 ? 1 : 0;
+			return `${v.toFixed(decimalDigits)}${field.unit}`;
 		},
 		color: v => {
 			let buckets = Object.keys(field.gaugeColours)

+ 64 - 29
web-view/src/js/processData.test.ts

@@ -1,51 +1,86 @@
-import {Reading, Values} from './types';
-import {splitData} from './processData';
+import {Minutes, Reading, TimeSeries, Timestamp, Values} from './types';
+import {processData, splitData} from './processData';
 import should = require('should');
 
 describe('Process Data', () => {
-	it('Splits data into parts', () => {
-		const input:Reading[] = createTestData(1);
-
-		const expected:Values = {
-			temperature: [{
-				value: input[0].temperature,
-				time: input[0].time
-			}],
-			humidity: [{
-				value: input[0].humidity,
-				time: input[0].time
-			}],
-			pressure: [{
-				value: input[0].pressure,
-				time: input[0].time
-			}],
-			light: [{
-				value: input[0].light,
-				time: input[0].time
-			}],
-		};
-
-		const actual = splitData(input);
-		should(actual).deepEqual(expected);
+	describe('Split Data', () => {
+		it('Splits data into streams', () => {
+			const input: Reading[] = createTestData(1);
+
+			const expected: Values = {
+				temperature: [{
+					value: input[0].temperature,
+					time: input[0].time
+				}],
+				humidity: [{
+					value: input[0].humidity,
+					time: input[0].time
+				}],
+				pressure: [{
+					value: input[0].pressure,
+					time: input[0].time
+				}],
+				light: [{
+					value: input[0].light,
+					time: input[0].time
+				}],
+				delay: [{
+					value: 0,
+					time: input[0].time
+				}],
+			};
+
+			const actual = splitData(input);
+			should(actual).deepEqual(expected);
+		});
+	});
+
+	it('extracts a delay time series', () => {
+		const alignWindow = 3;
+
+		const input: Reading[] = createTestData(12, [2, 2, 2, 3, 2.25, 2, 2, 5, 3, 2, 2, 2])
+
+		const expectedMaxDelays = ['2.0', '2.0', '3.0', '2.3', '2.0', NaN, '5.0', '3.0', '2.0', '2.0'];
+
+		const expectedMaxSeries = [];
+		for(let i = 0; i < expectedMaxDelays.length; i++) {
+			const time = (i+1) * alignWindow * 60 * 1000;
+			expectedMaxSeries.push({
+				time,
+				value: expectedMaxDelays[i]
+			});
+		}
+
+		const output: Values = processData(input, {
+			alignWindow
+		});
+
+		should(output.delay).deepEqual(expectedMaxSeries);
 	});
 });
 
 function createTestData(
 	numElement:number,
+	timeIncrements: Minutes[] = [15],
 	temperature:number = 5.2,
 	humidity:number = 45.5,
 	pressure:number = 995.6,
-	light:number = 120,
-	timeIncrement:number = 900000 // 15 minutes
+	light:number = 120
 ) {
 	let testData:Reading[] = [];
+	let time: Timestamp = 0;
 	for(let i = 0; i < numElement; i++) {
+		const delay: Minutes = i < timeIncrements.length
+			? timeIncrements[i]
+			: timeIncrements[timeIncrements.length-1];
+		time += delay * 60 * 1000;
 		testData.push({
 			temperature,
 			humidity,
 			pressure,
 			light,
-			time: (i+1) * timeIncrement
+			time,
+			delay: null,
 		});
 	}
 	return testData;

+ 15 - 2
web-view/src/js/processData.ts

@@ -54,7 +54,7 @@ export function processData(
 		series = field.canSpike
 			? removeSpikes(series, config.minSpikeChange, config.minSpikeMultiplier)
 			: series;
-		series = alignToWindow(series, config.alignWindow);
+		series = alignToWindow(series, config.alignWindow, field.windowType);
 		values[key] = series;
 	}
 
@@ -63,10 +63,23 @@ export function processData(
 
 export function splitData(data:Reading[]): Values {
 	const ret = {};
+
+	// Process delays first so the FIELD_KEYS bit works.
+	for(let i = 0; i < data.length; i++) {
+		const d = data[i];
+		if (i == 0) {
+			d.delay = 0;
+		} else {
+			d.delay = (d.time - data[i-1].time)/60/1000;
+			if (i == 1) {
+				data[0].delay = d.delay;
+			}
+		}
+	}
+
 	for(let field of FIELD_KEYS) {
 		ret[field] = data.map(d => splitValue(d, field));
 	}
-
 	return ret as Values;
 }
 

+ 15 - 4
web-view/src/js/types.d.ts

@@ -1,3 +1,8 @@
+/** Unix timestamp. */
+export type Timestamp = number;
+
+export type Minutes = number;
+
 /**
  * Record of a reading from the weather station.
  * Effectively, the raw data from the database.
@@ -9,7 +14,10 @@ export type Reading = {
 	pressure: number,
 
 	/** Unix timestamp of when this data was recorded. */
-	time: number
+	time: Timestamp,
+
+	/** Calculated delay, in minutes, since the previous reading. */
+	delay: Minutes,
 };
 
 /**
@@ -17,7 +25,7 @@ export type Reading = {
  */
 export type TimeSeries = {
 	value: number,
-	time: number
+	time: Timestamp
 };
 
 /**
@@ -29,5 +37,8 @@ export type Values = {
 	temperature: TimeSeries[],
 	humidity: TimeSeries[],
 	light: TimeSeries[],
-	pressure: TimeSeries[]
-}
+	pressure: TimeSeries[],
+
+	/** The maximum delay, in minutes, between samples during the window. */
+	delay: TimeSeries[],
+};

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott