Connect Power BI

Load Haltian IoT Parquet data into Power BI Desktop from OneLake or Azure Blob Storage and create visualizations.

This guide shows how to connect Power BI Desktop to the Parquet data imported by the Azure Function, create data tables, set up relationships, and build visualizations. Choose the section matching your deployment path.

Option A: Connect from OneLake (Lakehouse)

Prepare Lakehouse Tables

Before Power BI can use the data, convert the raw Parquet files in your Lakehouse into tables.

  1. Go to https://app.fabric.microsoft.com/ and open your Workspace → Lakehouse

    Select Workspace

  2. Select the Lakehouse from the workspace

    Select Lakehouse

  3. In the Files section, right-click the device folder → Load to TablesNew Table

    Load to Tables

  4. Set table name to device, file type to Parquet, and click Load

    Configure Table

  5. Repeat for each measurement table:

    • measurementCO2
    • measurementOccupantsCount
    • measurementOccupancyStatus

Connect Power BI Desktop to Lakehouse

  1. Open Power BI Desktop → NewBlank Report

    New Report

  2. Sign in to your Azure account (top-right corner)

  3. Select Get Data from another source → Microsoft FabricLakehousesConnect

    Connect to Lakehouse

  4. Select your Lakehouse and choose Connect to SQL endpoint from the Connect dropdown

    SQL Endpoint

  5. Select the tables to import and click Load

    Select Tables

  6. Choose Import to bring data into Power BI Desktop

    Import Mode

  7. Tables are now visible in the Data pane

    Tables Loaded

  8. Save the report with Ctrl+S

    Save Report


Option B: Connect from Azure Blob Storage

Connect Power BI Desktop to Storage Account

  1. Open Power BI Desktop → NewBlank Report

    New Report

  2. Select Get Data from another source → Azure Blob StorageConnect

    Azure Blob Storage

  3. Enter the storage account name (make sure you’re signed in to Azure in Power BI Desktop)

    Storage Account Name

  4. Select the container and click Load

    Select Container

  5. A raw table is loaded into the report

    Raw Table

Create Data Tables with Power Query

The raw blob data needs to be separated into logical tables. Select the table and click Transform Data to open Power Query Editor.

Transform Data

Device Table

  1. In Power Query Editor, right-click in the Queries pane → New QueryBlank Query

    New Query

  2. Rename the query to device, right-click → Advanced Editor

    Advanced Editor

  3. Paste the following Power Query M code:

let
    Source = AzureStorage.Blobs("YOUR_STORAGE_ACCOUNT_NAME"),
    IncomingContainer = Source{[Name = "incoming"]}[Data],
    FilteredParquet =
        Table.SelectRows(
            IncomingContainer,
            each [Extension] = ".parquet"
                 and Text.Contains([Name], "device")
        ),
    AddParquetTable =
        Table.AddColumn(
            FilteredParquet,
            "ParquetTable",
            each Parquet.Document([Content])
        ),
    KeepOnlyTableColumn =
        Table.SelectColumns(AddParquetTable, {"ParquetTable"}),
    Expanded =
        Table.ExpandTableColumn(
            KeepOnlyTableColumn,
            "ParquetTable",
            Table.ColumnNames(KeepOnlyTableColumn[ParquetTable]{0})
        ),
    Typed =
        Table.TransformColumnTypes(
            Expanded,
            {{"updatedTs", type datetime}}
        ),
    Grouped =
        Table.Group(
            Typed,
            {"id"},
            {{"Latest", each Table.Max(_, "updatedTs"), type record}}
        ),
    ColumnsToExpand = List.RemoveItems(Table.ColumnNames(Typed), {"id"}),
    Result =
        Table.ExpandRecordColumn(Grouped, "Latest", ColumnsToExpand)
in
    Result

Measurement Tables

Create additional blank queries for each measurement type using the same pattern. Change the Text.Contains filter and column types for each table.

measurementOccupancyStatus:

let
    Source = AzureStorage.Blobs("YOUR_STORAGE_ACCOUNT_NAME"),
    IncomingContainer = Source{[Name = "incoming"]}[Data],
    FilteredParquet =
        Table.SelectRows(
            IncomingContainer,
            each [Extension] = ".parquet"
                 and Text.Contains([Name], "measurementOccupancyStatus")
        ),
    AddParquetTable =
        Table.AddColumn(
            FilteredParquet, "ParquetTable",
            each Parquet.Document([Content])
        ),
    KeepOnlyTableColumn =
        Table.SelectColumns(AddParquetTable, {"ParquetTable"}),
    Expanded =
        Table.ExpandTableColumn(
            KeepOnlyTableColumn, "ParquetTable",
            Table.ColumnNames(KeepOnlyTableColumn[ParquetTable]{0})
        ),
    ChangedType =
        Table.TransformColumnTypes(
            Expanded,
            {{"status", type number}, {"ts", type datetime}},
            "en-US"
        )
in
    ChangedType

measurementOccupantsCount:

let
    Source = AzureStorage.Blobs("YOUR_STORAGE_ACCOUNT_NAME"),
    IncomingContainer = Source{[Name = "incoming"]}[Data],
    FilteredParquet =
        Table.SelectRows(
            IncomingContainer,
            each [Extension] = ".parquet"
                 and Text.Contains([Name], "measurementOccupantsCount")
        ),
    AddParquetTable =
        Table.AddColumn(
            FilteredParquet, "ParquetTable",
            each Parquet.Document([Content])
        ),
    KeepOnlyTableColumn =
        Table.SelectColumns(AddParquetTable, {"ParquetTable"}),
    Expanded =
        Table.ExpandTableColumn(
            KeepOnlyTableColumn, "ParquetTable",
            Table.ColumnNames(KeepOnlyTableColumn[ParquetTable]{0})
        ),
    ChangedType =
        Table.TransformColumnTypes(
            Expanded,
            {{"occupantsCount", type number}, {"ts", type datetime}},
            "en-US"
        )
in
    ChangedType

measurementCO2:

let
    Source = AzureStorage.Blobs("YOUR_STORAGE_ACCOUNT_NAME"),
    IncomingContainer = Source{[Name = "incoming"]}[Data],
    FilteredParquet =
        Table.SelectRows(
            IncomingContainer,
            each [Extension] = ".parquet"
                 and Text.Contains([Name], "measurementCO2")
        ),
    AddParquetTable =
        Table.AddColumn(
            FilteredParquet, "ParquetTable",
            each Parquet.Document([Content])
        ),
    KeepOnlyTableColumn =
        Table.SelectColumns(AddParquetTable, {"ParquetTable"}),
    Expanded =
        Table.ExpandTableColumn(
            KeepOnlyTableColumn, "ParquetTable",
            Table.ColumnNames(KeepOnlyTableColumn[ParquetTable]{0})
        ),
    ChangedType =
        Table.TransformColumnTypes(
            Expanded,
            {{"co2", type number}, {"ts", type datetime}},
            "en-US"
        )
in
    ChangedType

After creating all queries, click Close & Apply to save.

Close and Apply

Save the report with Ctrl+S.

Save Report


Create Data Relationships

Regardless of which data source you used, set up relationships between the device table and measurement tables.

  1. Switch to Model view and open Manage relationships

    Model View

  2. Click New to create a relationship

    New Relationship

  3. Select device as the primary table and measurementCO2 as the related table. Map id (device) → deviceId (measurement)

    Configure Relationship

  4. Repeat for measurementOccupantsCount and measurementOccupancyStatus

  5. The final model should look like this:

    Data Model


Create Visualizations

CO2 Line Chart

  1. Switch to Report view

    Report View

  2. Select Line Chart and resize the placeholder

    Line Chart

  3. From the Data pane, drag co2 from measurementCO2, ts, and name from device to the visual

    Drag Fields

  4. Change ts from Date Hierarchy to the raw ts value using the dropdown

    Change ts Type

  5. The chart displays CO2 readings per device over time

    CO2 Chart

Occupants Count Bar Chart

  1. Add a new page, select a Bar Chart

  2. Drag occupantsCount (use Max aggregation) and ts (Day level from Date Hierarchy)

  3. This shows maximum occupants per day

    Occupants Chart

Occupancy Status Matrix

  1. Add a new page, select a Matrix visual

  2. Drag name (device), status, and ts to the visual

    Matrix Visual

  3. Use the level buttons to drill into the date hierarchy

    Matrix Levels


Incremental Refresh

For large datasets, configure incremental refresh to avoid re-importing all historical data:

  1. Define RangeStart and RangeEnd parameters (DateTime type) in Power Query
  2. Filter the ts column using these parameters
  3. Set the refresh policy (e.g., store 12 months, refresh last 3 days)

Key Columns

ColumnTypeDescription
tsdatetimeMeasurement timestamp (UTC)
deviceIdstringDevice identifier
idstringDevice ID (in device table)
namestringDevice display name
co2numberCO2 concentration (ppm)
statusnumberOccupancy status (0 = free, 1 = occupied)
occupantsCountnumberNumber of occupants detected

See the Data API documentation for the full list of available measurement types and columns.

Next Steps

  • Publish the report to Power BI Service for sharing
  • Configure scheduled refresh in Power BI Service
  • Add additional measurement types as needed
  • Build custom dashboards for different stakeholder groups