Bygg ditt eget luftkvalitetssystem med Raspberry Pi og Azure

Jan Sviland
Systek
Published in
22 min readApr 26, 2023

Som utvikler må du ofte sette seg inn i nye domener, spesielt som IT-konsulent. I mitt nyeste oppdrag fikk jeg jobb hos Miljødirektoratet hvor vi skal lage et nytt system for luftkvalitet i Norge. Norge har 120 målestasjoner over hele landet som har målt luftkvalitet siden 1978. Faktisk er vi best i verden på måling av luftkvalitet!

Kanskje har du sett en slik målestasjon uten at du har tenkt over det, de ser slik ut:

Målestasjon i Sofienberg parken

Disse stasjonene måler svevestøv, utslipp av nitrogendioksid, NO2 og en rekke andre typer luftforurensning.

Jeg skal innrømme at jeg ikke er noe ekspert på målesensorer eller luftkvalitet, men man kan alltid lære! Og jeg har funnet at den beste måten å lære på er å bygge noe fra bunnen av. Derfor gikk jeg til anskaffelse av en Raspberry Pi og et måleinstrument for partikkelstøv for å bygge min helt egen målestasjon!

I tillegg til å lære om hvordan måling av luftkvalitet fungerer, tenkte jeg også å bruke denne muligheten til å ta et dypdykk i mange av tjenestene Azure tilbyr. Blant annet “Azure IoT Hub”, “Azure Stream Analytics”, “Azure Blob Storage”, “Azure SQL Database”, “Power BI” osv. Disse tjenestene kan enkelt settes opp til å ta imot store menger sensor-data og lagre dem.

Min ide er å sette opp noe slikt:

Raspberry Pi -> Azure IoT Hub -> Azure Stream Analytics -> Azure Blob Storage
Azure Stream Analytics -> Azure SQL Database - > Power BI
Azure Stream Analytics -> Azure Data Lake (RAW Data) -> Machine Learning

En raspberry Pi (med en partikkelmåler koblet til), sender data til Azure IoT Hub. Fra IoT hub kan man sette opp en “stream”, denne strømmen med data kan videresendes til flere tjenester i Azure, blant annet Azure Blob Storage og Azure SQL Database.

Men først, må målestasjonen bygges!

Steg 1: Sette opp en Raspberry Pi og koble til sensor

Det vi trenger er en

  • Raspberry Pi
  • Nova PM Sensor (SDS011)
  • Plastbeholder
  • Gaffateip :-)
Her kan dere se min Raspberry Pi, koblet til en Nova PM Sensor

For å gjøre målestasjonen litt finere, legger jeg alt sammen inn i en plastbeholder.

Endelig resultat:

Jans målestasjon

Dama syntes denne er kjempefin og er veldig fornøyd med at den pryder kjøkkenbenken… ´:-D

Steg 2: Vis data fra sensor

For å teste at alt fungerer trenger vi å se at måledata blir plukket opp og vise det. Jeg fant et eksempel på GitHub, hvor du kan bruke et enkelt python script til å hente ut måledata.

Med litt modifikasjoner lagde jeg dette:

import serial, time, datetime

ser = serial.Serial('/dev/ttyUSB0')

while True:
data = []
for index in range(0,10):
datum = ser.read()
data.append(datum)

pmtwofive = int.from_bytes(b''.join(data[2:4]), byteorder='little') / 10
pmten = int.from_bytes(b''.join(data[4:6]), byteorder='little') / 10

currentTime = datetime.datetime.now()

print(f"Time: {currentTime}, Data point: pm25 = {pmtwofive}, pm10 = {pmten}")
time.sleep(10)

Vi har en evig loop som kjører. Vi leser data fra “/dev/ttyUSB0”, det er USB-porten jeg har koblet målesensoren til. Det er litt triksing for å få ut pm2.5 og pm10 verdiene på en lesbar måte. Så er det bare å printe resultatet, og repeat.

Dette gir følgende resultat:

Koblet til Raspberry Pi og kjører python script for å vise måledata

Woho!

Data måles, data vises, og det ser ut til å vise en ny verdi hvert minutt. Som passer bra. Her kan vi se tidspunkt, og verdi for PM2.5 og PM10.

Du kan finne koden her:

https://github.com/jansviland/pi_air_quality_monitor/blob/main/scripts/getMeasurement-continuously.py

Jeg kan også betrygge dere med at en PM2.5-konsentrasjonen på 1,2 µg/m³ (mikrogram per kubikkmeter), betyr at det er en lav konsentrasjon av fint støv i luften. Det er meget god luftkvalitet på kjøkkenet mitt med andre ord!

Så hva er PM2.5?

PM2.5 refererer til partikulær materie (PM) som har en diameter på 2,5 mikrometer eller mindre. Disse små partiklene er også kjent som fint støv. PM2.5-partikler er så små at de kan trenge dypt inn i lungene — og til og med nå blodbanen. De stammer fra ulike kilder, inkludert bilutslipp, industriutslipp, skogbranner, og andre forbrenningsprosesser.

Eksponering for PM2.5 kan ha en rekke negative helseeffekter, spesielt for barn, eldre og personer med eksisterende hjerte- og lungesykdommer. Noen av de potensielle helseeffektene inkluderer økt risiko for luftveissykdommer, forverring av astma og bronkitt, samt økt risiko for hjerteinfarkt og slag.

Luftkvalitetsstandarder og retningslinjer, som de fra Verdens helseorganisasjon (WHO) og US Environmental Protection Agency (EPA), inkluderer grenseverdier for PM2.5 for å beskytte folkehelsen. Overvåking av PM2.5-nivåer er viktig for å informere beslutningstakere og befolkningen om luftkvaliteten og bidra til å redusere risikoen for helseproblemer knyttet til luftforurensning.

Størrelsen på svevestøv

Hva er PM10?

PM10 refererer til partikulær materie (PM) som har en diameter på 10 mikrometer eller mindre. Disse partiklene er også kjent som grovt støv. PM10-partikler er større enn PM2.5-partikler, men er fortsatt små nok til å inhaleres og trenge inn i lungene. Kildene til PM10-partikler inkluderer veistøv, jordforstyrrelser, industriutslipp, skogbranner, og noen biologiske partikler som pollen og sporer.

Selv om PM10-partikler er mindre farlige enn PM2.5-partikler, kan eksponering for PM10 også ha negative helseeffekter, spesielt for barn, eldre og personer med eksisterende hjerte- og lungesykdommer. Noen av de potensielle helseeffektene inkluderer irritasjon i øyne, nese og hals, forverring av astma og bronkitt, og økt risiko for hjerte- og lungesykdommer.

Steg 3: Sende data til Azure

Tilbake til utviklingen!

Nå som Målestasjonen er satt opp, og vi har verifisert at luftkvalitet blir målt riktig, kan vi gå videre til å lagre data.

Min plan er å logge all disse dataene og laste det opp til Azure sin Skytjeneste. Det finnes en egen tjeneste i Azure for IoT (Internet of Things), som er ment for denne type data. For bare 10$ i måneden kan du sende 300.000 requests med data hver dag til et endepunkt i Azure og logge store mengder med data fra sensorer som dette.

De har også en gratis tjeneste som er begrenset til 8.000 request per dag. Dette holder for mitt formål, siden jeg bare tenker å sende data hvert minutt fra en sensor, som da blir 1440 request per dag. (24 timer * 60 minutt = 1440)

Dermed kan jeg uten problem sette denne sensoren til å sende data, uten å dra på meg store kostnader (eller kan jeg det??). Vi starter med å lage en “IoT Hub”.

Sett opp “IoT Hub” i Azure

Jeg setter den til “Free”, som begrenser meldinger per dag til 8.000, det er mer en nok for mitt bruk.

Legg til device

Deretter må man sette opp “devices”. Jeg lager en ny “device” som jeg kaller for “raspberry-pi-jan”. Dette er for autentisering av min Raspberry Pi. Azure trenger å vite hva som kobler til og vi trenger også et passord eller en “connection string”, som er unik for min Raspberry Pi.

Hent “connection string” og send data til Azure sin skytjeneste

Etter å ha satt opp dette, får vi info om min nye klient, “raspberry-pi-jan”. En “connection string” har blitt generert. Med denne koden kan vi sette opp min Raspberry Pi til å koble seg til Azure og sende data.

For å få til dette gjør jeg noen små modifikasjoner til mitt opprinnelige python script:


import datetime
import os
import asyncio
import time
from azure.iot.device.aio import IoTHubDeviceClient
import serial
import portalocker

ser = serial.Serial('/dev/ttyUSB0')

CLIENT_ID = "raspberry-pi-jan"
CONNECTION_STRING = os.getenv("IOTHUB_DEVICE_CONNECTION_STRING")
JSON_PAYLOAD = '{{"pm2": {pm2}, "pm10": {pm10}, "client_id": "{client_id}"}}'
CSV_PAYLOAD = '{pm2},{pm10},{client_id},{time}'
CSV_HEADER = 'pm2,pm10,client_id,time'

def save_data_to_file(currentTime, data):

year, month, day = currentTime.year, currentTime.month, currentTime.day
base_path = f"{year}/{month:02d}/{day:02d}"

os.makedirs(base_path, exist_ok=True)
file_path = os.path.join(base_path, "measurements.csv")

timeout = 20 # Timeout in seconds

try:
# Check if the file exists, and write the header if it doesn't
if not os.path.exists(file_path):
with portalocker.Lock(file_path, mode="w", timeout=timeout) as f:
f.write(CSV_HEADER)
f.write("\n")

# Append the data to the file
with portalocker.Lock(file_path, mode="a", timeout=timeout) as f:
f.write(data)
f.write("\n")

print(f"Saved data: \"{data}\" to file: \"{file_path}\"")

except portalocker.exceptions.LockTimeout:
print(f"Could not acquire lock on {file_path} within {timeout} seconds")


async def main():

# Create instance of the device client using the authentication provider
device_client = IoTHubDeviceClient.create_from_connection_string(CONNECTION_STRING)

# Connect the device client.
await device_client.connect()

while True:
json = []
for index in range(0,10):
datum = ser.read()
json.append(datum)

# get the data from the sensor
pmtwofive = int.from_bytes(b''.join(json[2:4]), byteorder='little') / 10
pmten = int.from_bytes(b''.join(json[4:6]), byteorder='little') / 10
currentTime = datetime.datetime.now()

print(f"Time: {currentTime}, Data point: pm25 = {pmtwofive}, pm10 = {pmten}, client_id = {CLIENT_ID}")

# Create the JSON and CSV payloads
json = JSON_PAYLOAD.format(pm2=pmtwofive, pm10=pmten, client_id=CLIENT_ID)
cvs = CSV_PAYLOAD.format(pm2=pmtwofive, pm10=pmten, client_id=CLIENT_ID, time=currentTime)

# Save data to file
save_data_to_file(currentTime, cvs)

# Send a message to the IoT hub
print(f"Sending message: {json}")
await device_client.send_message(json)

# time.sleep(10)

# finally, shut down the client
await device_client.shutdown()

if __name__ == "__main__":
asyncio.run(main())

Jeg har lagd en ny metode save_data_to_file, den kommer til å være viktig senere. Her lagres data slik “/2023/04/11/measurements.csv”. Jeg legger til en ny linje i filen for hver måling som blir gjort. Og lager en ny csv fil hver dag, med målinger for den dagen, med en mappe for år, måned og dag.

Deretter setter jeg opp Client_ID, Connection_string og JSON_PAYLOAD etc. Dette er for kobling til Azure og definisjon på hvordan data skal struktureres. Både som JSON og CSV. For Azure så sender jeg JSON og for å lagre lokal backup, så bruker jeg CSV (comma separated values).

Scriptet kobler først til Azure IoT hub og starter en evig loop hvor vi henter data fra sensoren, deretter lagrer data lokalt og sender pm2.5 og pm10 verdiene til Azure.

Det eneste vi trenger å gjøre for å kjøre dette er å kopiere “connection string” (vist over) og kjøre

export IOTHUB_DEVICE_CONNECTION_STRING='HostName=air-mo... Din hemmelige kode ...'

Dette vil sette en environment variabel med connection string som vi kan gjenbruke i koden. Deretter kan vi bare starte python scriptet i bakgrunnen på Raspberry Pi slik:

nohup python -u /home/pi/git/pi_air_quality_monitor/scripts/sendTestDataToAzure.py >> azurelog.log &

Dette vil starte python scriptet i bakgrunnen og logge output til filen azurelog.log

Får se output fra python scriptet vårt kan vi printe ut output lagt til i azurelog.log slik

tail -f azurelog.log

Dette vil vise noe slikt:


Time: 2023-04-09 15:36:09.278294, Data point: pm25 = 2.4, pm10 = 11.3, client_id = raspberry-pi-jan
Saved data: "2.4,11.3,raspberry-pi-jan,2023-04-09 15:36:09.278294" to file: "2023/04/09/measurements.csv"
Sending message: {"pm2": 2.4, "pm10": 11.3, "client_id": "raspberry-pi-jan"}
Time: 2023-04-09 15:37:09.756959, Data point: pm25 = 2.7, pm10 = 9.5, client_id = raspberry-pi-jan
Saved data: "2.7,9.5,raspberry-pi-jan,2023-04-09 15:37:09.756959" to file: "2023/04/09/measurements.csv"
Sending message: {"pm2": 2.7, "pm10": 9.5, "client_id": "raspberry-pi-jan"}
Time: 2023-04-09 15:38:10.234620, Data point: pm25 = 2.8, pm10 = 11.6, client_id = raspberry-pi-jan
Saved data: "2.8,11.6,raspberry-pi-jan,2023-04-09 15:38:10.234620" to file: "2023/04/09/measurements.csv"
Sending message: {"pm2": 2.8, "pm10": 11.6, "client_id": "raspberry-pi-jan"}
Time: 2023-04-09 15:39:10.710908, Data point: pm25 = 2.8, pm10 = 9.5, client_id = raspberry-pi-jan
Saved data: "2.8,9.5,raspberry-pi-jan,2023-04-09 15:39:10.710908" to file: "2023/04/09/measurements.csv"
Sending message: {"pm2": 2.8, "pm10": 9.5, "client_id": "raspberry-pi-jan"}
Time: 2023-04-09 15:40:11.189842, Data point: pm25 = 2.7, pm10 = 10.2, client_id = raspberry-pi-jan
Saved data: "2.7,10.2,raspberry-pi-jan,2023-04-09 15:40:11.189842" to file: "2023/04/09/measurements.csv"
Sending message: {"pm2": 2.7, "pm10": 10.2, "client_id": "raspberry-pi-jan"}
Time: 2023-04-09 15:41:11.665916, Data point: pm25 = 2.3, pm10 = 8.0, client_id = raspberry-pi-jan
Saved data: "2.3,8.0,raspberry-pi-jan,2023-04-09 15:41:11.665916" to file: "2023/04/09/measurements.csv"
Sending message: {"pm2": 2.3, "pm10": 8.0, "client_id": "raspberry-pi-jan"}

Nå kan vi bare la dette kjøre og vår Raspberry Pi vil logge data 24/7 og laste opp data hvert minutt.

Azure Oppsett

Nå som vi sender data til Azure trenger vi å håndtere data som kommer inn. Vi gjør dette ved å sette opp en “ Stream Analytics job”, her definerer vi input (vår IoT stream), en query og en output. Ganske enkelt hvor vi henter data fra, hvilke data vi skal filtrere ut og hvor dette skal sendes.

I første omgang kan vi ganske enkelt lagre data som kommer inn. For eksempel ved å bruke Azure Blob Storage (som er et billig alternativ for å lagre data over lang tid). Det vil se slik ut:

Partikkelmåler -> Raspberry Pi -> IoT Stream -> Blob Storage

For input velger vi bare IoTHubInput. Det eneste vi trenger å endre på her er formatet. Vi ønsker å sende inn data hvert minutt og lage en liste/array med alle måleverdiene. Så vi velger “Array”. Det neste jeg endrer på er “Path pattern”, der skriver jeg bare {date}. Dette vil lage en mappe hver for år, måned og dag. For hver dag vil det lagres en ny json fil med alle måleverdiene for en dag, i riktig mappe.

Når vi setter en Stream Analytics Job til å logge til Blob Storage er det viktig å sette format Array og path til {date}

Vi må også sette opp en query:

SELECT
*
INTO
[BlobOutput]
FROM
[IoTHubInput]

Eksempel på output i blob storage:

Her ser vi at det er opprettet en mappe for år “2023”, i den mappen har vi mappe for februar “02”, og under den mappen har vi mapper for hver dag, her ser vi data for “07”. Altså dato 2023.02.07. Som du ser kommer en haug med data inn, hvert minutt.

Vi kan også lage flere “streams” fra vår input. For eksempel kan vi lage query som henter verdier hvert minutt den siste timen og regne ut gjennomsnittet for pm2.5 og pm10 den siste timen. Deretter kan vi lagre snitt verdien hver time.

SQL Stream

Vi kan også sette opp Azure Stream til å logge til en SQL Database. Det gjøres på samme måte som over, bortsett fra noen endringer i query delen:

SELECT
DATEDIFF(s, '1970-01-01 00:00:00', EventEnqueuedUtcTime) as unixtime,
pm2,
pm10,
client_id,
EventEnqueuedUtcTime,
EventProcessedUtcTime,
PartitionId

INTO
[measurements]
FROM
[air-monitor-hub]

Jeg legger til en liten kodesnippet for å regne ut unixtime. Vi har allerede DateTime verdier for når eventet blir sendt, men det er ofte lettere å forholde seg til UnixTime, som ganske enkelt er antall sekunder fra 01.01.1970 UTC. Siden dette bare er et enkelt nummer er det veldig lett å sortere på. Derfor har jeg noe logikk for å også lagre dette i en egen kolonne.

Jeg spesifiserer også kolonnen jeg vil ha eksplisitt. På denne måten blir det ikke lagret noe mer data enn jeg ønsker. Siden data blir sendt hvert minutt er det greit å begrense det så mye som mulig. For BlobStorage er lagring veldig billing, men for SQL databaser så koster det litt om den trenger mye plass.

En ting som funger utmerket er hvordan Azure automatisk oppdager hvilke data som blir mottatt og hvilke type hver variabel er.

Her er et eksempel på hva Azure oppdager automatisk. Her har jeg lagt til et nytt datapunkt, en string med klient-navnet, dette mangler i SQL databasen. Da kan vi enkelt oppdatere SQL databasen i Azure og en ny kolonne med “client_name” blir opprettet automatisk.

SQL tabell blir opprettet automatisk basert på data som kommer inn

Steg 4: Lag en applikasjon for visning av data

Nå som vi sender data hvert minutt til skylagringstjenesten vår og data lagres både i SQL og BlobStorage, kan vi fortsette med å lage en visning. Det mest typiske og logiske er å lage en web applikasjon, men denne gangen tenker jeg å lage en windows/mac app. Ved å bruke Avalonia UI kan vi lage applikasjoner som kjører på både windows, mac og Linux. Personlig bruker jeg både Windows, Linux og Mac så det er et krav for min del.

https://www.avaloniaui.net/

Det neste jeg trenger er en måte å vise data på, i form av en fin graf. Det er et open source bibliotek for å vise grafer og plotter her som ser veldig bra ut, ScottPlot. Så jeg går for det.

I demoprosjektet deres er det forskjellige eksempler på hvordan vi kan plotte data og vise det som en graf. Her er et eksempel som kan passe bra for mitt formål:

Plotting DateTime Data on Signal Plot

Det viser en fin graf, med nøyaktig tidspunkt under. Det er en god start.

Vi starter med å opprette et nytt prosjekt:

Jeg er ingen ekspert på Avalonia UI, men jeg kan lage en enkel visning slik:

    <Grid ColumnDefinitions="Auto,*">
<ListBox Name="MenuListBox"
Grid.Column="0"
Items="{Binding MenuItems}"
SelectedItem="{Binding SelectedMenuItem}"
Width="200"
HorizontalAlignment="Stretch"
BorderThickness="0">

<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"
FontWeight="Bold"
Margin="5"
/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

<scottPlot:AvaPlot Grid.Column="1" Name="AvaPlot1" />

</Grid>

Dette definerer en meny på venstre side som er 200 px bred og en plot på høyre side som strekker seg så stort som vinduet er.

Jeg starter med å generere random data som jeg plotter inn:

var avaPlot = this.FindControl<AvaPlot>("AvaPlot1");
avaPlot.Plot.Clear();
avaPlot.Plot.Title(menuItem.Name);

// create data sample data
double[] ys = DataGen.RandomWalk(_rand, PointCount);
TimeSpan ts = TimeSpan.FromSeconds(1); // time between data points
double sampleRate = (double)TimeSpan.TicksPerDay / ts.Ticks;
var signalPlot = avaPlot.Plot.AddSignal(ys, sampleRate);

// Then tell the axis to display tick labels using a time format
avaPlot.Plot.XAxis.DateTimeFormat(true);

// Set start date
signalPlot.OffsetX = new DateTime(1985, 10, 1).ToOADate();

avaPlot.Render();

Vi finner vårt ScottPlot element AvaPlot1 og fjerner eksisterende innhold med Clear(). Vi setter tittel på grafen og generer random verdier for y.

Da får vi dette:

En god start. Det neste vi trenger å gjøre er å hente data fra Azure.

For å hente data fra vår SQL database kan vi gjøre en enkel spørring slik:

private List<Measurement> GetMeasurements(int count)
{
List<Measurement> measurements = new();
using (var con = new SqlConnection(_connectionString))
{
con.Open();

using (var command = new SqlCommand($"SELECT top({count}) * FROM measurements order by unixtime desc", con))
{
var reader = command.ExecuteReader();
while (reader.Read())
{
measurements.Add(new Measurement()
{
Pm2 = reader.GetDouble(0),
Pm10 = reader.GetDouble(1),
EventEnqueuedUtcTime = reader.GetDateTime(4),

// unixtime can be null in the database, added it later on, so in some earlier rows it will be null
UnixTime = reader.IsDBNull(6) ? null : reader.GetInt64(6),
});
}
}
}

return measurements;
}

Her tar vi de siste 60 verdiene fra SQL databasen og lager en liste med Measurement objekter.

Deretter kan vi plotte dette inn i visningen:

var avaPlot = this.FindControl<AvaPlot>("AvaPlot1");
avaPlot.Plot.Clear();
avaPlot.Plot.Title(menuItem.Name);

// convert the the measurements to arrays
var xs = new double[_measurements.Count];
var pm2 = new double[_measurements.Count];
var pm10 = new double[_measurements.Count];

for (var i = 0; i < _measurements.Count; i++)
{
xs[i] = _measurements[i].EventEnqueuedUtcTime.ToOADate();
pm2[i] = _measurements[i].Pm2;
pm10[i] = _measurements[i].Pm10;
}

// add the measurements to the plot
avaPlot.Plot.AddScatter(xs, pm2, label: "PM2");
avaPlot.Plot.AddScatter(xs, pm10, label: "PM10");
avaPlot.Plot.Legend();

// tell the axis to display tick labels using a time format
avaPlot.Plot.XAxis.DateTimeFormat(true);

avaPlot.Render();

Her bruker vi listen med “measurments”. Vi må gjøre en konvertering av verdiene våre til tre forskjellige arrays med verdier. Én får tidspunkt, EventEnqueuedUtcTime, én får pm2.5 og én får pm10. Deretter legger vi inn disse nye verdiene for å lage to grafer.

Resultatet blir slik:

Her kan vi se at noe skjedde kl 10:00 i går, litt usikker på hva, men det var et øyeblikk med mye grovt støv som ble plukket opp på kjøkkenet. Ellers har nivået holdt seg lavt.

Steg 4: Gjør om applikasjonen til noe det faktisk går an å bruke

Så langt viser vi kun de siste 60 verdiene som er målt. Dette er fint, men ikke så veldig nyttig. Det er et par ekstra features som jeg tenker er nødvendig for å faktisk få noe ut av denne applikasjonen.

  1. Hent data fra BlobStorage/SQL databasen og lagre denne informasjonen lokalt. Så vi slipper å hente ut den samme informasjonen igjen og igjen (og slipper å dra på flere kostnader for skytjenester enn nødvendig)
  2. Lag funksjonalitet for å vise målinger for gitte dager. Man bør kunne velge en gitt dag og få opp data for den dagen. Ikke bare vise de siste 60 målingene som jeg gjør nå.
  3. Det burde være en “live” funksjon som oppdaterer grafen hvert minutt. “Luftkvalitet på kjøkkenet til Jan, minutt for minutt” om du vil. Dette vil nok kreve at jeg enabler streaming av data i Azure, noe som koster litt for mye å ha på 24/7, men kan enable det midlertidig og teste det.

Her er resultatet etter litt mer arbeid

Jeg har lagt til en kalendervisning hvor vi kan se målinger for ulike dager, samt et live-view hvor vi kan se måledata minutt for minutt.

Forbedret grensesnitt, kan se data de forskjellige dagene og gå tilbake i tid.

Det er litt mer kode en hva jeg har vist her. Hele prosjektet kan du finne her:

Power BI, lag et dashboard uten å skrive noe kode!

Man kan komme langt nå til dags med automatiske verktøy. Med Power BI kan vi enkelt lage eget dashboard, samt legge til hvilke grafer og tabeller vi ønsker. Alt med et veldig enkelt drag-and-drop interface. Bare velg hvilke data du vil ha og hvordan du vil vise det.

På bare et par minutter lagde jeg dette:

Power BI, brukervennlig verktøy for å håndtere data

Dette fikk jeg opp uten å skrive en eneste linje med kode. Jeg logget bare inn i Azure via Power BI → valgte databasen min → valgte “Scatter chart” i Power BI — og vips så fikk jeg en fin graf.

Det er sikkert veldig mye mer du kan gjøre i Power BI for å lage avanserte og detaljerte Dashboard. Med koordinater så kunne vi for eksempel vist alle målestasjoner på et kart.

Kostnad, ikke så billig som jeg trodde…

Da jeg startet dette prosjektet antok jeg at det nærmest var gratis. Blant annet skulle du kunne gjøre 8.000 requests til “IoT hub” gratis hver dag. Du kan sette opp blob storage nesten gratis, og en 2 GB SQL database gratis.

Dette var noe misvisende. Det viser seg at selve “IoT hub” koster lite/ingenting. Men å sende “output” videre til BlobStorage og SQL Databasen (som selvfølgelig er nødvendig — noe må vi gjøre med data som kommer inn), det koster en del. Etter bare én uke hadde jeg brukt over 600 kr av mine 1800 kr i gratis credits.

Å lagre til blob storage koster ikke så mye, så langt bare 24 kr. Men å strømme data til SQL Databasen kostet over 500 kr på bare en uke (!). Det er fint å kunne se luftkvaliteten på kjøkkenet, men jeg kan ikke betale 2000 kr i måneden for det…

Dette viser at man skal være litt forsiktig med å ukritisk sette opp skytjenester, det er ikke alltid lett å vite hva kostnadene blir. Selv når man er konservativ og setter alt til det billigste alternativet.

Kostnader etter en uke, strømme data til en SQL database koster litt
Kostnad etter litt over en måned: her kan vi se at 60% er SQL-stream, 1176 kr. Vi kan også se at bare å strømme data til blob storage kostet 600 kr på kort tid.

Heldigvis så finnes det en enkel måte å få kostnadene ned til null. Når monitor-stream ikke kjører, koster det ingenting. Og når du skrur på en “stream” så kan den plukke opp fra der den stoppet. Dermed kan du skru på en stream i 5 minutter, en gang i måneden, og få all data. Deretter skru den av igjen. På den måten blir kostnaden nærmest null, og du får inn all måledata.

Men, dette er ikke optimalt… Vi må logge inn i Azure hver gang, skru på tjenesten, vente, skru den av.. yuk! Det er en manuell og kjedelig prosess. Det er også fort gjort å glemme å skru av tjenesten, og før du vet ordet av det så drar du på deg kostnader på 100 kr dagen uten er klar over det.

Her kan dere se en illustrasjon av hva som skjer når du glemmer å skru av tjenesten en dag:

Kosta meg 100 kr å ikke trykke på av-knappen…

Så, for å ikke bli ruinert må vi finne en bedre løsning!

PS. Det går an å automatisere dette ved å bruke: https://learn.microsoft.com/en-us/cli/azure/stream-analytics/job?view=azure-cli-latest#az-stream-analytics-job-start, men det er litt hacky, det nok ikke meningen at IoT Hub skal bli brukt på denne måten. Best å finne en annen løsning.

Alternativ til IoT Hub for enkeltpersoner / gjerrige personer

Om du har flere målestasjoner og sensorer, så egner kanskje IoT Hub seg veldig bra. For større prosjekter blir kostnadene for IoT Hub lave, totalt sett. Det blir en viss minimums-kostnad fordi en del tjenester må kjøre 24/7, men om du har 100 sensorer, så er det ikke sikkert at det blir så mye dyrere enn å ha én sensor, slik som jeg har.

Men for mitt bruk er det ikke nødvendig. En enklere og billigere løsning er å lagre data lokalt, og så kjøre en bulk insert en gang hver kveld. Direkte til Databasen.

Så, etter å ha brukt en del tid på å sette opp IoT Hub, for så å innse at det koster litt for mye å ha en tjeneste som kjører 24/7. For deretter å innse at jeg heller ikke bare kan skru på tjenesten en gang i blant for å oppdatere databasen, må jeg tilbake til tegnebrettet og finne en ny løsning.

Jeg lagrer allerede data lokalt med python scriptet her:

(...)

year, month, day = currentTime.year, currentTime.month, currentTime.day
base_path = f"{year}/{month:02d}/{day:02d}"

os.makedirs(base_path, exist_ok=True)
file_path = os.path.join(base_path, "measurements.csv")

(...)

Dette lager en ny mappe for år, måned og dag, og lager en fil “measurements.csv”. Innholdet blir noe sånt:

\2023\04\11\measurements.csv

pm2,pm10,client_id,time
2.4,4.6,raspberry-pi-jan,2023-04-11 00:00:11.906024
2.1,4.6,raspberry-pi-jan,2023-04-11 00:01:12.372237
2.2,4.1,raspberry-pi-jan,2023-04-11 00:02:12.839793
2.2,3.9,raspberry-pi-jan,2023-04-11 00:03:13.309732
2.2,5.6,raspberry-pi-jan,2023-04-11 00:04:13.777878
2.1,4.3,raspberry-pi-jan,2023-04-11 00:05:14.244584
2.0,3.8,raspberry-pi-jan,2023-04-11 00:06:14.708095
2.2,5.1,raspberry-pi-jan,2023-04-11 00:07:15.170929
2.2,4.9,raspberry-pi-jan,2023-04-11 00:08:15.632008
2.4,4.5,raspberry-pi-jan,2023-04-11 00:09:16.087141
2.2,5.3,raspberry-pi-jan,2023-04-11 00:10:16.545116
2.0,4.7,raspberry-pi-jan,2023-04-11 00:11:17.012813
2.3,5.9,raspberry-pi-jan,2023-04-11 00:12:17.476365
2.2,5.3,raspberry-pi-jan,2023-04-11 00:13:17.938952
2.6,6.8,raspberry-pi-jan,2023-04-11 00:14:18.400191
2.2,4.6,raspberry-pi-jan,2023-04-11 00:15:18.860157
2.2,4.0,raspberry-pi-jan,2023-04-11 00:16:19.320080
2.1,4.2,raspberry-pi-jan,2023-04-11 00:17:19.782698
2.3,4.3,raspberry-pi-jan,2023-04-11 00:18:20.244927
2.2,5.4,raspberry-pi-jan,2023-04-11 00:19:20.708870
1.9,4.1,raspberry-pi-jan,2023-04-11 00:20:21.172680
2.0,4.4,raspberry-pi-jan,2023-04-11 00:21:21.636295

Alt jeg trenger er å lagre dette i Databasen. Denne gangen uten IoT Hub.

Jeg setter opp en ny database slik:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[values]
(
[Guid] [uniqueidentifier] NOT NULL DEFAULT NEWID(),
[pm2] [float] NOT NULL,
[pm10] [float] NOT NULL,
[UtcTime] [datetime2](7) NOT NULL UNIQUE,
[UnixTime] [bigint] NULL,
[ClientId] [varchar](255) NOT NULL
) ON [PRIMARY]
GO

CREATE CLUSTERED INDEX [idx_UtcTime] ON [dbo].[values]
([UtcTime] DESC) WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF) ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [idx_UnixTime] ON [dbo].[values]
([UnixTime] DESC) WITH (STATISTICS_NORECOMPUTE = OFF, DROP_EXISTING = OFF, ONLINE = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO

Deretter har jeg lagd et C# Console prosjekt som tar en .csv fil og legger alle verdiene inn i databasen. Slik:

    public void Run(string[] input)
{
_logger.LogInformation("Input contains {Input} values", input.Length);

var measurements = new List<Measurement>();
for (var i = 1; i < input.Length; i++)
{
var split = input[i].Split(',');
measurements.Add(new Measurement()
{
Pm2 = Convert.ToDouble(split[0], CultureInfo.InvariantCulture),
Pm10 = Convert.ToDouble(split[1], CultureInfo.InvariantCulture),
ClientId = split[2],
EventEnqueuedUtcTime = DateTime.ParseExact(split[3], "yyyy-MM-dd HH:mm:ss.ffffff", CultureInfo.InvariantCulture)
});
}

BulkInsert(measurements);
}

private void BulkInsert(List<Measurement> measurements)
{
using var connection = new SqlConnection(_connectionString);
connection.Open();

DataTable table = new DataTable();
table.TableName = "values";

table.Columns.Add("Guid", typeof(Guid));
table.Columns.Add("pm2", typeof(double));
table.Columns.Add("pm10", typeof(double));
table.Columns.Add("UtcTime", typeof(DateTime));
table.Columns.Add("UnixTime", typeof(long));
table.Columns.Add("ClientId", typeof(string));

foreach (var measurement in measurements)
{
var row = table.NewRow();

row["Guid"] = Guid.NewGuid();
row[nameof(Measurement.Pm2)] = measurement.Pm2;
row[nameof(Measurement.Pm10)] = measurement.Pm10;
row["UtcTime"] = measurement.EventEnqueuedUtcTime;
row[nameof(Measurement.UnixTime)] = measurement.EventEnqueuedUtcTime.ToUnixTime();
row[nameof(Measurement.ClientId)] = measurement.ClientId;

table.Rows.Add(row);
}

using (var bulkInsert = new SqlBulkCopy(_connectionString))
{
bulkInsert.DestinationTableName = table.TableName;
bulkInsert.WriteToServer(table);
}
}

Jeg kjører ganske enkelt dette automatisk hver kveld. Man kan sette opp en såkalt CRON (Command Run On) job i Linux slik:

* * * * * command-to-be-executed
- - - - -
| | | | |
| | | | ----- Day of week (0 - 7) (Sunday is both 0 and 7)
| | | ------- Month (1 - 12)
| | --------- Day of the month (1 - 31)
| ----------- Hour (0 - 23)
------------- Minute (0 - 59)

Det jeg trenger er bare å kjøre “AirQuality.Console” applikasjonen, med en fil som input. Siden det er “measurements.csv” filen som ble laget dagen før, må jeg ta gårsdagens dato i dette formatet /2023/04/10.

Jeg kan lage et shell script, et script laget for å utføre kommandoer på Linux operativsysteme. Raspberry Pi kjører Linux, så det er passende.

run_air_quality_monitor.sh

#!/bin/sh

# Store the date for yesterday
yesterday=$(date -d yesterday "+%Y/%m/%d")

# Change to the application directory
cd ~/git/pi_air_quality_monitor/src/AirQuality.Console/bin/Debug/net6.0

# Run the .NET application with the yesterday's date
/usr/local/bin/dotnet AirQuality.Console.dll ~/${yesterday}/measurements.csv >> ~/AirQuality.Console.log 2>&1

Dette vil gjøre følgende:

  1. “date -d yesterday “+%Y/%m/%d”, returnerer gårsdagens dato slik som dette: 2023/04/10.
  2. “cd ~/git/pi_air_quality_monitor/…” Navigerer til mappen med prosjektet
  3. “dotnet AirQuality.Console.dll”, kjører programmet

Vi legger det inn som en CRON job, slik:


0 4 * * * ~/git/pi_air_quality_monitor/scripts/run_air_quality_monitor.sh

“0 4 * * *” Betyr kjør kl 04:00 hver natt.

Med dette oppsettet vil alle de ca 1440 måleverdiene målt i løpet av en dag bli lastet opp automatisk hver natt kl 04:00. Easy peachy.

Konklusjon

For de som har klart seg helt til siste slutt. Tusen takk for at du leste dette blogginnlegget, jeg håper det ga deg noe kunnskap eller inspirasjon.

Målestasjonen har tikket og gått i et par måneder nå, hvert minutt, uten noen problemer. Azure IoT Hub har også fungert bra, selv om kostnadene var noe misledende til å begynne med. Jeg har fremdeles ikke funnet noen lur måte å få live data på.

Jeg kunne spart meg for en del tid hvis jeg droppet hele IoT hub og gikk for en alternativ løsning med en gang, men sånn er det med IT prosjekter. Man vet ikke hvilke utfordringer man møter underveis. Det er derfor tidsestimering av prosjekter er nærmest umulig. Fordi jeg ikke tidligere har satt opp et IoT Hub prosjekt så kunne jeg ikke forutsett at dette var en “blindvei” og at jeg senere måtte skifte retning og finne en alternativ løsning.

Hvor verdifullt det er å ha en luftkvalitetsmåler hjemme er noe du må vurdere selv. Om du eller noen i husholdningen er allergisk mot støv eller har astma så kan det være en god ide. Personlig har det bare vært et hobbyprosjekt for meg. Men jeg har oppdaget hva som skaper dårlig luft hjemme og har nå et mer bevisst forhold til det. For eksempel å skru på vifta når du steker mat på kjøkkenet og hold vindu ut mot veien lukket! Robotstøvsuger anbefales også, min kjører hver dag, så det er vært lite støv hos meg.

Det skal også sies at all mengde støv er dårlig for deg, så om man kan få det ned på et lavt nivå så er dette en god ide. Det er ikke uten grunn at Norge har 120 målestasjoner rundt i landet og måler dette svært nøye. Alle EU/EØS-land er pålagt å ha dette. EU og Miljødirektoratet i Norge tar dette på alvor.

Det europeiske miljøbyrået, EEA, har estimert at mellom 248 000 og 489 000 for tidlige dødsfall i EU i 2016 kunne tilskrives eksponering for fint svevestøv (PM2,5), mens tilsvarende tall for Norge er oppgitt å være omtrent 1 300 (EEA, 2019).

Ca. 4,1 millioner dødsfall på verdensbasis(!).

Kan lese mer om det her: https://www.miljodirektoratet.no/globalassets/publikasjoner/m1669/m1669.pdf

Når det er sagt så har Norge veldig god luftkvalitet. Som jeg startet denne artikkelen med, er vi best på luftmåling og er på topp 3 når det gjelder luftkvalitet. Så vi trenger ikke bekymre seg for mye her i landet.

Link til prosjektet

Hvis du er interessert i å sette opp et tilsvarende prosjekt finner du hele prosjektet her, med en del dokumentasjon:

Mulig det blir gjort litt forbedringer etter at dette er publisert.

Og du, har du lyst til å bli min kollega? Sjekk ut Systek.no.

Bilder

Her er flere bilder av den endelige målestasjonen :)

--

--