Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1c5bc07
feat: preserve sensor metadata after device creation
jona159 Aug 10, 2026
82908b7
feat: add metadata to sensor definitions
jona159 Aug 10, 2026
2c89a61
feat: add test to persist sensor definition id in sensor data
jona159 Aug 10, 2026
90619e2
feat: turn sensor definitions into routing catalog
jona159 Aug 12, 2026
05f1a44
feat: add nc values to luftdaten.info model definition
jona159 Aug 12, 2026
b81befe
feat: route by definition id
jona159 Aug 12, 2026
563147c
feat: decoder tests
jona159 Aug 12, 2026
3d4ba49
fix: rm generic lufdaten.info device model
jona159 Aug 17, 2026
4f31528
fix: only concrete model definitions
jona159 Aug 17, 2026
3a76349
feat: allow only generic luftdaten.info model, rm concrete models
jona159 Aug 17, 2026
ecb0fd6
fix: adjust example data
jona159 Aug 17, 2026
1aba09d
feat: add sen55 definitions
jona159 Aug 18, 2026
9c23664
feat: pass sensor templates
jona159 Aug 18, 2026
60fbc0d
feat: use id instead of title
jona159 Aug 18, 2026
837d53e
feat: rm concrete models in favor of generic
jona159 Aug 18, 2026
a09a757
feat: use device model zod enum, refine
jona159 Aug 18, 2026
1b78016
feat: check sensor data
jona159 Aug 18, 2026
f3a4d56
feat: warnings instead of throwing
jona159 Aug 18, 2026
34d8daa
feat: use device model zod enum, refine
jona159 Aug 18, 2026
b0fd1e9
fix: variable name
jona159 Aug 20, 2026
09f9f22
feat: add ui feedback for conflicting sensors
jona159 Aug 20, 2026
2a112d4
fix: replace outdated variable name with inline list
jona159 Aug 21, 2026
a179724
Merge branch 'dev' into fix/decoder-substring-matching
jona159 Aug 21, 2026
7dfe96e
fix: improve check
jona159 Aug 21, 2026
1d8eefc
fix: reject infinity values
jona159 Aug 21, 2026
8a4ac2b
fix: tests
jona159 Aug 24, 2026
4afab93
fix: tests
jona159 Aug 24, 2026
f9f6be9
Merge branch 'dev' into fix/decoder-substring-matching
scheidtdav Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions app/db/models/device.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export function getDeviceForMeasurementWrite({ id }: Pick<Device, 'id'>) {
id: true,
title: true,
sensorType: true,
data: true,
},
},
},
Expand Down Expand Up @@ -979,6 +980,8 @@ export async function createDevice(deviceData: any, userId: string) {
let storedDeviceSchemaVersion = null
const isCustomDevice =
!deviceData.model || deviceData.model?.toLowerCase() === 'custom'
const usesSensorDefinitions =
Boolean(deviceData.model) && !isCustomDevice && !deviceData.sensors

// If model and sensors are both specified, reject (backwards compatibility)
if (
Expand Down Expand Up @@ -1089,6 +1092,24 @@ export async function createDevice(deviceData: any, userId: string) {
sensorsToAdd.length > 0
) {
for (const [index, sensorData] of sensorsToAdd.entries()) {
const existingSensorData =
sensorData.data &&
typeof sensorData.data === 'object' &&
!Array.isArray(sensorData.data)
? sensorData.data
: {}
const sensorMetadata = storedDeviceSchemaVersion
? {
...existingSensorData,
deviceSchemaSensorId: sensorData.id,
}
: usesSensorDefinitions
? {
...existingSensorData,
sensorDefinitionId: sensorData.id,
}
: sensorData.data

const [newSensor] = await tx
.insert(sensor)
.values({
Expand All @@ -1100,9 +1121,7 @@ export async function createDevice(deviceData: any, userId: string) {
sensorWikiPhenomenon: sensorData.sensorWikiPhenomenon,
sensorWikiUnit: sensorData.sensorWikiUnit,
deviceId: createdDevice.id,
data: storedDeviceSchemaVersion
? { deviceSchemaSensorId: sensorData.id }
: sensorData.data,
data: sensorMetadata,
order: sensorData.order ?? index,
})
.returning()
Expand Down
5 changes: 5 additions & 0 deletions app/lib/model-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ export const modelDefinitions = {
'sps30_pm4',
'sps30_pm10',
'sps30_pm25',
'sps30_nc05',
'sps30_nc1',
'sps30_nc25',
'sps30_nc4',
'sps30_nc10',
'sht3x_humidity',
'sht3x_temperature',
'bmp180_temperature',
Expand Down
271 changes: 270 additions & 1 deletion app/lib/sensor-definitions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
export const sensorDefinitions = {
type SensorDefinition = {
title: string
unit: string
sensorType: string
icon: string
image?: string
phenomenon?: string
decoderMappings?: {
luftdaten?: readonly DecoderValueMapping[]
}
}

export type DecoderValueMapping = {
valueType: string
multiplier?: number
}

const sensorDefinitionTemplates = {
windspeed: {
title: 'Windgeschwindigkeit',
unit: 'm/s',
Expand Down Expand Up @@ -254,6 +271,41 @@ export const sensorDefinitions = {
icon: 'osem-cloud',
image: '/img/sensor_images/SPS30.jpg',
},
sps30_nc05: {
title: 'NC0.5',
unit: '#/cm³',
sensorType: 'SPS30',
icon: 'osem-cloud',
image: '/img/sensor_images/SPS30.jpg',
},
sps30_nc1: {
title: 'NC1.0',
unit: '#/cm³',
sensorType: 'SPS30',
icon: 'osem-cloud',
image: '/img/sensor_images/SPS30.jpg',
},
sps30_nc25: {
title: 'NC2.5',
unit: '#/cm³',
sensorType: 'SPS30',
icon: 'osem-cloud',
image: '/img/sensor_images/SPS30.jpg',
},
sps30_nc4: {
title: 'NC4.0',
unit: '#/cm³',
sensorType: 'SPS30',
icon: 'osem-cloud',
image: '/img/sensor_images/SPS30.jpg',
},
sps30_nc10: {
title: 'NC10',
unit: '#/cm³',
sensorType: 'SPS30',
icon: 'osem-cloud',
image: '/img/sensor_images/SPS30.jpg',
},
pms1003_pm10: {
title: 'PM10',
unit: 'µg/m³',
Expand Down Expand Up @@ -338,4 +390,221 @@ export const sensorDefinitions = {
icon: 'osem-humidity',
image: '/img/sensor_images/HDC1008.png',
},
} as const satisfies Record<string, SensorDefinition>

type SensorDefinitionMetadata = {
phenomenon: string
decoderMappings?: SensorDefinition['decoderMappings']
}

const sensorDefinitionMetadata = {
windspeed: { phenomenon: 'wind-speed' },
dht22_temperature: {
phenomenon: 'air-temperature',
decoderMappings: {
luftdaten: [
{ valueType: 'temperature' },
{ valueType: 'DHT22_temperature' },
],
},
},
bme680_temperature: { phenomenon: 'air-temperature' },
smt50_soilmoisture: { phenomenon: 'soil-moisture' },
sht3x_temperature: {
phenomenon: 'air-temperature',
decoderMappings: { luftdaten: [{ valueType: 'SHT3X_temperature' }] },
},
pms5003_pm01: {
phenomenon: 'particulate-matter-mass-concentration-1um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P0' }] },
},
pms5003_pm25: {
phenomenon: 'particulate-matter-mass-concentration-2.5um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P2' }] },
},
bme280_pressure_pa: {
phenomenon: 'atmospheric-pressure',
decoderMappings: { luftdaten: [{ valueType: 'BME280_pressure' }] },
},
bme680_humidity: { phenomenon: 'relative-humidity' },
bme280_humidity: {
phenomenon: 'relative-humidity',
decoderMappings: { luftdaten: [{ valueType: 'BME280_humidity' }] },
},
pms5003_pm10: {
phenomenon: 'particulate-matter-mass-concentration-10um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P1' }] },
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
bme280_temperature: {
phenomenon: 'air-temperature',
decoderMappings: { luftdaten: [{ valueType: 'BME280_temperature' }] },
},
veml6070_uvintensity: { phenomenon: 'ultraviolet-intensity' },
sht3x_humidity: {
phenomenon: 'relative-humidity',
decoderMappings: { luftdaten: [{ valueType: 'SHT3X_humidity' }] },
},
bme680_pressure: { phenomenon: 'atmospheric-pressure' },
tsl45315_lightintensity: { phenomenon: 'illuminance' },
bmp180_temperature: {
phenomenon: 'air-temperature',
decoderMappings: { luftdaten: [{ valueType: 'BMP180_temperature' }] },
},
sds011_pm25: {
phenomenon: 'particulate-matter-mass-concentration-2.5um',
decoderMappings: { luftdaten: [{ valueType: 'SDS_P2' }] },
},
sps30_pm10: {
phenomenon: 'particulate-matter-mass-concentration-10um',
decoderMappings: { luftdaten: [{ valueType: 'SPS30_P1' }] },
},
soundlevelmeter: { phenomenon: 'sound-pressure-level' },
pms7003_pm25: {
phenomenon: 'particulate-matter-mass-concentration-2.5um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P2' }] },
},
bme680_voc: { phenomenon: 'volatile-organic-compounds' },
dnms_la_min: { phenomenon: 'sound-pressure-level-minimum' },
dht22_humidity: {
phenomenon: 'relative-humidity',
decoderMappings: {
luftdaten: [{ valueType: 'humidity' }, { valueType: 'DHT22_humidity' }],
},
},
pms7003_pm01: {
phenomenon: 'particulate-matter-mass-concentration-1um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P0' }] },
},
dps310_temperature: { phenomenon: 'air-temperature' },
pms7003_pm10: {
phenomenon: 'particulate-matter-mass-concentration-10um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P1' }] },
},
sps30_pm25: {
phenomenon: 'particulate-matter-mass-concentration-2.5um',
decoderMappings: { luftdaten: [{ valueType: 'SPS30_P2' }] },
},
dps310_pressure: { phenomenon: 'atmospheric-pressure' },
bme280_pressure_hpa: {
phenomenon: 'atmospheric-pressure',
decoderMappings: {
luftdaten: [{ valueType: 'BME280_pressure', multiplier: 0.01 }],
},
},
bmp180_pressure_pa: {
phenomenon: 'atmospheric-pressure',
decoderMappings: { luftdaten: [{ valueType: 'BMP180_pressure' }] },
},
sds011_pm10: {
phenomenon: 'particulate-matter-mass-concentration-10um',
decoderMappings: { luftdaten: [{ valueType: 'SDS_P1' }] },
},
sps30_pm1: {
phenomenon: 'particulate-matter-mass-concentration-1um',
decoderMappings: { luftdaten: [{ valueType: 'SPS30_P0' }] },
},
pms1003_pm01: {
phenomenon: 'particulate-matter-mass-concentration-1um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P0' }] },
},
hdc1080_temperature: { phenomenon: 'air-temperature' },
pms1003_pm25: {
phenomenon: 'particulate-matter-mass-concentration-2.5um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P2' }] },
},
scd30_co2: {
phenomenon: 'carbon-dioxide-concentration',
decoderMappings: {
luftdaten: [{ valueType: 'SCD30_co2' }, { valueType: 'SCD30_co2_ppm' }],
},
},
hdc1008_temperature: { phenomenon: 'air-temperature' },
sps30_pm4: {
phenomenon: 'particulate-matter-mass-concentration-4um',
decoderMappings: { luftdaten: [{ valueType: 'SPS30_P4' }] },
},
sps30_nc05: {
phenomenon: 'particle-number-concentration-0.5um',
decoderMappings: { luftdaten: [{ valueType: 'SPS30_N05' }] },
},
sps30_nc1: {
phenomenon: 'particle-number-concentration-1um',
decoderMappings: { luftdaten: [{ valueType: 'SPS30_N1' }] },
},
sps30_nc25: {
phenomenon: 'particle-number-concentration-2.5um',
decoderMappings: { luftdaten: [{ valueType: 'SPS30_N25' }] },
},
sps30_nc4: {
phenomenon: 'particle-number-concentration-4um',
decoderMappings: { luftdaten: [{ valueType: 'SPS30_N4' }] },
},
sps30_nc10: {
phenomenon: 'particle-number-concentration-10um',
decoderMappings: { luftdaten: [{ valueType: 'SPS30_N10' }] },
},
pms1003_pm10: {
phenomenon: 'particulate-matter-mass-concentration-10um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P1' }] },
},
pms3003_pm25: {
phenomenon: 'particulate-matter-mass-concentration-2.5um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P2' }] },
},
bmp180_pressure_hpa: {
phenomenon: 'atmospheric-pressure',
decoderMappings: {
luftdaten: [{ valueType: 'BMP180_pressure', multiplier: 0.01 }],
},
},
dnms_la_max: { phenomenon: 'sound-pressure-level-maximum' },
hdc1080_humidity: { phenomenon: 'relative-humidity' },
pms3003_pm01: {
phenomenon: 'particulate-matter-mass-concentration-1um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P0' }] },
},
dht11_temperature: {
phenomenon: 'air-temperature',
decoderMappings: {
luftdaten: [
{ valueType: 'temperature' },
{ valueType: 'DHT11_temperature' },
],
},
},
smt50_soiltemperature: { phenomenon: 'soil-temperature' },
dht11_humidity: {
phenomenon: 'relative-humidity',
decoderMappings: {
luftdaten: [{ valueType: 'humidity' }, { valueType: 'DHT11_humidity' }],
},
},
pms3003_pm10: {
phenomenon: 'particulate-matter-mass-concentration-10um',
decoderMappings: { luftdaten: [{ valueType: 'PMS_P1' }] },
},
dnms_la_eq: {
phenomenon: 'equivalent-continuous-sound-pressure-level',
decoderMappings: { luftdaten: [{ valueType: 'DNMS_noise_LAeq' }] },
},
bmp280_pressure: { phenomenon: 'atmospheric-pressure' },
hdc1008_humidity: { phenomenon: 'relative-humidity' },
} as const satisfies Record<
keyof typeof sensorDefinitionTemplates,
SensorDefinitionMetadata
>

type SensorDefinitions = {
[Key in keyof typeof sensorDefinitionTemplates]: (typeof sensorDefinitionTemplates)[Key] &
(typeof sensorDefinitionMetadata)[Key]
}

export const sensorDefinitions = Object.fromEntries(
Object.entries(sensorDefinitionTemplates).map(([id, definition]) => [
id,
{
...definition,
...sensorDefinitionMetadata[id as keyof typeof sensorDefinitionMetadata],
},
]),
) as SensorDefinitions
Loading
Loading