Skip to content

Commit

Permalink
Fix slider and moisture values
Browse files Browse the repository at this point in the history
  • Loading branch information
Dennis960 committed Sep 3, 2024
1 parent 6417b55 commit 5a0c7d6
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 120 deletions.
52 changes: 44 additions & 8 deletions Server/src/lib/components/sensor-settings-form.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand All @@ -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,
Expand All @@ -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' }
]
};
});
Expand Down Expand Up @@ -230,7 +254,7 @@

<style>
.slider {
padding-bottom: 2.5rem;
padding-bottom: 8rem;
}
.slider :global(.noUi-connects) {
Expand All @@ -248,4 +272,16 @@
.slider :global(.noUi-connects > div:nth-child(3)) {
background: var(--tblr-warning);
}
.slider :global(.noUi-value-horizontal) {
-webkit-transform: translate(-50%, 25px);
transform: translate(-50%, 25px);
writing-mode: vertical-lr;
}
.slider :global(.noUi-tooltip) {
display: none;
}
.slider :global(.noUi-active .noUi-tooltip) {
display: block;
}
</style>
4 changes: 3 additions & 1 deletion Server/src/lib/components/slider.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
(bucket <= sensorConfig.lowerThreshold * sensorConfig.fieldCapacity ||
bucket >= sensorConfig.upperThreshold * sensorConfig.fieldCapacity);
const sensorValueMax = 1024;
const sensorValueMax = 2500;
const distributionMap: Map<number, number> = new Map();
for (
let x = 0;
Expand Down
229 changes: 119 additions & 110 deletions Server/src/lib/server/repositories/SensorDataRepository.ts
Original file line number Diff line number Diff line change
@@ -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<number | undefined> {
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<number | undefined> {
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<number>`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<number>`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;
}
}

0 comments on commit 5a0c7d6

Please sign in to comment.