diff --git a/Server/src/lib/components/sensor-settings-form.svelte b/Server/src/lib/components/sensor-settings-form.svelte index 22f377c..638bdef 100644 --- a/Server/src/lib/components/sensor-settings-form.svelte +++ b/Server/src/lib/components/sensor-settings-form.svelte @@ -24,14 +24,16 @@ export let formAction = '/sensor'; export let formMethod: 'POST' | 'PUT' = 'POST'; + const MAX_FIELD_CAPACITY = 2500; + let initialConfig: SensorConfigurationDTO = config != undefined ? { ...config } : { name: funnyPlantNames[Math.floor(Math.random() * funnyPlantNames.length)], imageBase64: undefined, - fieldCapacity: 1024, - permanentWiltingPoint: 1024 * 0.3, + fieldCapacity: MAX_FIELD_CAPACITY, + permanentWiltingPoint: MAX_FIELD_CAPACITY * 0.3, upperThreshold: 0.8, lowerThreshold: 0.2 }; @@ -41,10 +43,23 @@ let sliderValues: (number | string)[] = []; $: [permanentWiltingPoint, lowerThreshold, upperThreshold, fieldCapacity] = sliderValues.map( - (v) => (typeof v === 'number' ? Math.floor(v) : Math.floor(parseFloat(v))) + (v) => Math.floor((parseFloat(v.toString()) / 100) * MAX_FIELD_CAPACITY) ); onMount(async () => { + const sliderLabels = ['Lufttrocken', 'Trocken', 'Feucht', 'Nass', 'Unter Wasser']; + const format = { + to: function (value: number) { + value = Math.floor((value / MAX_FIELD_CAPACITY) * (sliderLabels.length - 1)); + return sliderLabels[value]; + }, + from: function (value: string) { + if (!Number.isNaN(parseFloat(value))) { + return parseFloat(value); + } + return (sliderLabels.indexOf(value) / (sliderLabels.length - 1)) * MAX_FIELD_CAPACITY; + } + }; sliderOptions = { start: [ initialConfig.permanentWiltingPoint, @@ -53,12 +68,21 @@ initialConfig.fieldCapacity ], connect: true, - range: { min: [0], max: [1024] }, + range: { min: 0, max: MAX_FIELD_CAPACITY }, + step: MAX_FIELD_CAPACITY / ((sliderLabels.length - 1) * 5), + format: format, pips: { mode: PipsMode.Values, - density: 3, - values: [0, 250, 500, 750, 1000] - } + format: format, + values: sliderLabels.map(format.from), + density: 5 + }, + tooltips: [ + { to: () => 'Vertrocknet' }, + { to: () => 'Braucht Wasser' }, + { to: () => 'Zu viel Wasser' }, + { to: () => 'Überflutet' } + ] }; }); @@ -230,7 +254,7 @@ diff --git a/Server/src/lib/components/slider.svelte b/Server/src/lib/components/slider.svelte index 2429ecb..bba1821 100644 --- a/Server/src/lib/components/slider.svelte +++ b/Server/src/lib/components/slider.svelte @@ -18,7 +18,9 @@ onMount(async () => { const NoUiSlider = (await import('nouislider')).default; slider = NoUiSlider.create(container, options); - slider.on('update', (newValues) => { + slider.on('update', () => { + const newValues = slider.getPositions(); + if (JSON.stringify(newValues) === JSON.stringify(values)) return; values = newValues; dispatch('input', { values: newValues }); diff --git a/Server/src/lib/components/water-capacity-distribution.svelte b/Server/src/lib/components/water-capacity-distribution.svelte index 4020e8c..e051297 100644 --- a/Server/src/lib/components/water-capacity-distribution.svelte +++ b/Server/src/lib/components/water-capacity-distribution.svelte @@ -24,7 +24,7 @@ (bucket <= sensorConfig.lowerThreshold * sensorConfig.fieldCapacity || bucket >= sensorConfig.upperThreshold * sensorConfig.fieldCapacity); - const sensorValueMax = 1024; + const sensorValueMax = 2500; const distributionMap: Map = new Map(); for ( let x = 0; diff --git a/Server/src/lib/server/repositories/SensorDataRepository.ts b/Server/src/lib/server/repositories/SensorDataRepository.ts index ad56c4e..523fa20 100644 --- a/Server/src/lib/server/repositories/SensorDataRepository.ts +++ b/Server/src/lib/server/repositories/SensorDataRepository.ts @@ -1,124 +1,133 @@ -import { db } from "$lib/server/db/worker"; -import { sensorReadings } from "$lib/server/db/schema"; -import { and, eq, gte, lte, desc, sql, asc, count } from "drizzle-orm"; +import { sensorReadings } from '$lib/server/db/schema'; +import { db } from '$lib/server/db/worker'; +import { and, asc, count, desc, eq, gte, lte, sql } from 'drizzle-orm'; + +const MAX_FIELD_CAPACITY = 2500; + +function invertMoisture( + data: typeof sensorReadings.$inferSelect +): typeof sensorReadings.$inferSelect { + return { + ...data, + moisture: MAX_FIELD_CAPACITY - data.moisture + }; +} export default class SensorDataRepository { - /** - * Add data to the database. - * @param data The data to add. - * @returns The id of the inserted data or throws an error if the sensor does not exist. - */ - static async create( - data: typeof sensorReadings.$inferInsert - ): Promise { - data.date = new Date(); - const insertedRecord = await db - .insert(sensorReadings) - .values({ ...data }) - .returning({ id: sensorReadings.id }); + /** + * Add data to the database. + * @param data The data to add. + * @returns The id of the inserted data or throws an error if the sensor does not exist. + */ + static async create(data: typeof sensorReadings.$inferInsert): Promise { + data.date = new Date(); + const insertedRecord = await db + .insert(sensorReadings) + .values({ ...data }) + .returning({ id: sensorReadings.id }); - return insertedRecord[0]?.id; - } + return insertedRecord[0]?.id; + } - /** - * Takes a list of data points and averages them to the specified limit. - * The average of "water", "voltage", and "duration" are calculated. - * The average of "date" is the last date. - * If the limit is greater than the number of data points, the original data is returned. - * @param data The list of data to average. - * @param limit The final number of data points. - * @returns The averaged data. - */ - private static dataToAverage( - data: typeof sensorReadings.$inferSelect[], - limit: number - ): typeof sensorReadings.$inferSelect[] { - if (data.length <= limit) { - return data; - } - const averagedData: typeof sensorReadings.$inferSelect[] = []; - const step = Math.floor(data.length / limit); - for (let i = 0; i < data.length; i += step) { - const dataSlice = data.slice(i, i + step); + /** + * Takes a list of data points and averages them to the specified limit. + * The average of "water", "voltage", and "duration" are calculated. + * The average of "date" is the last date. + * If the limit is greater than the number of data points, the original data is returned. + * @param data The list of data to average. + * @param limit The final number of data points. + * @returns The averaged data. + */ + private static dataToAverage( + data: (typeof sensorReadings.$inferSelect)[], + limit: number + ): (typeof sensorReadings.$inferSelect)[] { + if (data.length <= limit) { + return data; + } + const averagedData: (typeof sensorReadings.$inferSelect)[] = []; + const step = Math.floor(data.length / limit); + for (let i = 0; i < data.length; i += step) { + const dataSlice = data.slice(i, i + step); - // Make a copy of the latest values - const averagedDataPoint = { ...dataSlice[dataSlice.length - 1] }; + // Make a copy of the latest values + const averagedDataPoint = { ...dataSlice[dataSlice.length - 1] }; - // Calculate averages in this bucket for summable attributes - const attributesToAverage = [ - "light", - "voltage", - "temperature", - "humidity", - "moisture", - "moistureStabilizationTime", - "humidityRaw", - "temperatureRaw", - "rssi", - "duration", - ] satisfies (keyof typeof dataSlice[0])[]; + // Calculate averages in this bucket for summable attributes + const attributesToAverage = [ + 'light', + 'voltage', + 'temperature', + 'humidity', + 'moisture', + 'moistureStabilizationTime', + 'humidityRaw', + 'temperatureRaw', + 'rssi', + 'duration' + ] satisfies (keyof (typeof dataSlice)[0])[]; - const calculateAverage = (property: keyof typeof dataSlice[0]) => - dataSlice.reduce((acc, cur) => acc + (cur[property] as number ?? 0), 0) / dataSlice.length; + const calculateAverage = (property: keyof (typeof dataSlice)[0]) => + dataSlice.reduce((acc, cur) => acc + ((cur[property] as number) ?? 0), 0) / + dataSlice.length; - for (const attribute of attributesToAverage) { - averagedDataPoint[attribute] = calculateAverage(attribute); - } + for (const attribute of attributesToAverage) { + averagedDataPoint[attribute] = calculateAverage(attribute); + } - averagedData.push(averagedDataPoint); - } - return averagedData; - } + averagedData.push(averagedDataPoint); + } + return averagedData; + } - /** - * Get data by sensor address. Newest data first. - * @param sensorAddress The address of the sensor. - * @param startDate The start date in ms since epoch. (optional) - * @param endDate The end date in ms since epoch. (optional) - * @param maxDataPoints The maximum number of data points. (optional) - * @returns The data or an empty list if sensor does not exist. - */ - static async getAllBySensorIdAveraged( - sensorAddress: number, - startDate: Date, - endDate: Date, - maxDataPoints: number - ) { - // Fetch data between startDate and endDate - const data = await db - .select() - .from(sensorReadings) - .where( - and( - eq(sensorReadings.sensorAddress, sensorAddress), - gte(sensorReadings.date, startDate), - lte(sensorReadings.date, endDate) - ) - ) - .orderBy(desc(sensorReadings.date)); + /** + * Get data by sensor address. Newest data first. + * @param sensorAddress The address of the sensor. + * @param startDate The start date in ms since epoch. (optional) + * @param endDate The end date in ms since epoch. (optional) + * @param maxDataPoints The maximum number of data points. (optional) + * @returns The data or an empty list if sensor does not exist. + */ + static async getAllBySensorIdAveraged( + sensorAddress: number, + startDate: Date, + endDate: Date, + maxDataPoints: number + ) { + // Fetch data between startDate and endDate + const data = ( + await db + .select() + .from(sensorReadings) + .where( + and( + eq(sensorReadings.sensorAddress, sensorAddress), + gte(sensorReadings.date, startDate), + lte(sensorReadings.date, endDate) + ) + ) + .orderBy(desc(sensorReadings.date)) + ).map(invertMoisture); - // Return the averaged data - return this.dataToAverage(data, maxDataPoints); - } + // Return the averaged data + return this.dataToAverage(data, maxDataPoints); + } - static async getCountByWaterCapacityBucket( - sensorId: number, - sinceDate: Date, - bucketSize: number - ) { - const dist = await db - .select({ - count: count(), - bucket: sql`floor(${sensorReadings.moisture} / ${bucketSize}) * ${bucketSize}` - }) - .from(sensorReadings) - .where(and( - eq(sensorReadings.sensorAddress, sensorId), - gte(sensorReadings.date, sinceDate) - )) - .groupBy(sensorReadings.moisture) - .orderBy((table) => asc(table.bucket)); + static async getCountByWaterCapacityBucket( + sensorId: number, + sinceDate: Date, + bucketSize: number + ) { + const dist = await db + .select({ + count: count(), + bucket: sql`floor((${MAX_FIELD_CAPACITY} - ${sensorReadings.moisture}) / ${bucketSize}) * ${bucketSize}` + }) + .from(sensorReadings) + .where(and(eq(sensorReadings.sensorAddress, sensorId), gte(sensorReadings.date, sinceDate))) + .groupBy(sensorReadings.moisture) + .orderBy((table) => asc(table.bucket)); - return dist; - } + return dist; + } }