使用MASA Stack+.Net 从零开始搭建IoT平台 第七章 查询历史数据
@
前言
IoT平台需要监控设备的运行状态,统计和分析设备传感器数据,使用图表展示是比较常见的场景。使用图表和表格数据组合的Dashboard也可以放在首页作为大屏展示。
分析
因为我们设备上报的数据都是存储到时序库influxdb中的,所以我们按照时间统计数据是很方便的,但是设备上报的数据频率和我们需要的统计周期可能并不一致,例如设备5s上报一次传感器数据,但是我们希望2小时统计一次这两小时内的最高值、最低值,或者平均值。查看文档发现aggregateWindow函数可以满足我们的需求,也就是所有的计算都可以在influxdb中完成。结合MASA Blazor现成的ECharts组件可以轻松完成图表制作。
方案
我们可以现在influxdb的UI管理界面点击DataExplorer中调试我们的查询脚本,我提前准备了一些数据
from(bucket: "IoTDemos") |> range(start: 2023-07-17T16:00:00Z,stop:2023-07-18T16:00:00Z) |> filter(fn: (r) => r._measurement == "AirPurifierDataPoint" and r.ProductId == "c85ef7e5-2e43-4bd2-a939-07fe5ea3f459" and r.DeviceName == "284202304230001") |> aggregateWindow(every: 2h, fn: mean) |> fill(value: 0.0)
我们这里只对接下来用到的一个查询语句做简单介绍,其他语法与函数请参考influxdb官方文档
- from(bucket: "IoTDemos")表示我们要查询的库。
- |> range(start: 2023-07-17T16:00:00Z,stop:2023-07-18T16:00:00Z) 代表我们查询的时间范围,这里时间范围还有很多写法,例如range(start: -10h)代表查询最近十小时之内的数据,这里有个需要注意的地方,range(start: -1d),可以这样写来查一天之内的数据,但是这一天是按照UTC时间来统计的。
- filter(fn: (r) => r._measurement == "AirPurifierDataPoint"
and r.ProductId == "c85ef7e5-2e43-4bd2-a939-07fe5ea3f459"
and r.DeviceName == "284202304230001")
filter过滤函数,可以限定我们的查询条件,这里的条件为从AirPurifierDataPoint表中,并限定了设备名称和产品ID,这里如果只查一个字段可以添加查询条件,例如添加 and r._field == "PM_25",将只返回PM_25的值 - |> aggregateWindow(every: 2h, fn: mean) 由于设备上报数据比较频繁(我这里模拟的数据是5s上报一次),但是当我展示给用户图表的时候,我不希望上面有那么密集的点,这时候就可以使用aggregateWindow函数,每2小时统计一次平均值,mean代表算数平均值。这样我查询24小时数据最多可以得到12个数据点,每个点为这两小时数据的平均值。
- |> fill(value: 0.0) 如果某个时间范围没有数据,那么这个时间范围统计出来的平均值会是"null",而不是0,我们可以使用这个函数将null替换为0.
我们执行一下,可以看到得到了36条数据(共3个字段,每个字段一天12条汇总数据)
编写代码
定义数据类
我们先定义一个ECharts需要的数据类
public class EChartsData { /// <summary> /// 设备名称 /// </summary> public string DeviceName { get; set; } public List<FieldData> FieldDataList { get; set; } } public class FieldData { /// <summary> /// 字段名称 /// </summary> public string FieldName { get; set; } /// <summary> /// 时间点列表(X轴) /// </summary> public List<DateTime> DateTimes { get; set; } /// <summary> /// 数据点列表(Y轴) /// </summary> public List<double> Values { get; set; } }
编写查询方法
然后我们在MASA.IoT.Core项目的TimeSeriesDbClient类中添加一个查询的方法
public async Task<EChartsData> GetDeviceDataPointListAsync(GetDeviceDataPointListOption option) { var query = $@"from(bucket: ""{_bucket}"") |> range(start: {option.UTCStartDateTimeStr},stop:{option.UTCStopDateTimeStr}) |> filter(fn: (r) => r._measurement == ""AirPurifierDataPoint"" and r.ProductId == ""{option.ProductId}"" and r.DeviceName == ""{option.DeviceName}"") |> aggregateWindow(every: 2h, fn: mean) |> fill(value: 0.0)"; var tables = await _client.GetQueryApi().QueryAsync(query, _org); var fieldList = tables.SelectMany(table => table.Records).Select(o => o.GetField()).Distinct(); var eChartsData = new EChartsData { DeviceName = option.DeviceName, FieldDataList = new List<FieldData>() }; var fluxRecords = tables.SelectMany(table => table.Records); foreach (var field in fieldList) { eChartsData.FieldDataList.Add(new FieldData { FieldName = field, DateTimes = fluxRecords.Where(o => o.GetField()== field).Select(o => o.GetTime().Value.ToDateTimeUtc()) .ToList(), Values = fluxRecords.Where(o => o.GetField() == field).Select(o => (double)o.GetValue()).ToList(), }); } return eChartsData; }
这里查询方法与写入类似,通过SDK提供的_client.GetQueryApi()方法获取查询api,然后通过QueryAsync方法查询我们拼凑的语句。查询的结果是tables集合,我们可以通过GetTime()和GetValue()方法来拿到时间和对应的值。
由于influxdb存储的时间都是UTC时间,所以查询条件需要转换成UTC时间,使用o.GetValue()获取到的是object类型,我们需要转换成double。
添加ECharts图表
接下来我们开始在UI项目中添加ECharts图表,第一步先在MASA.IoT.UI项目的_Host.cshtml文件中部分添加echarts的js文件
<script src="https://cdn.masastack.com/npm/echarts/5.1.1/echarts.min.js"></script>
使用MASA Blazor创建页面就相对简单很多了,首先有一个设备列表页面,展示设备的名称和在线状态,当点击设备右侧的按钮时,弹出抽屉页面,显示我们的ECharts图表,这里还使用了Tab组件,方便以后扩展设备相关其他功能
@page "/DeviceList" @using MASA.IoT.Core.Contract.Device @using MASA.IoT.UI.Components <PageTitle>设备列表</PageTitle> <h1>设备列表</h1> <MDataTable Headers="_headers" Items="deviceList" Class="elevation-1" Page="_options.PageIndex" ItemsPerPage="_options.PageSize" ServerItemsLength="_totalCount"> <ItemColContent> @if (context.Header.Value == "actions") { <MIcon Small Class="mr-2" OnClick="()=>EditItem(context.Item)">mdi-pencil</MIcon> } else if (context.Header.Value == nameof(DeviceListViewModel.OnLineStates)) { <EnumChip Value="context.Item.OnLineStates"></EnumChip> } else { @context.Value } </ItemColContent> </MDataTable> <PDrawer Width="1000" Value="ShowDrawer" ValueChanged="DrawerChangedAsync"> <ActivatorContent> </ActivatorContent> <ChildContent> <MTabs ValueChanged="TabsValueChanged"> <MTab>图表</MTab> </MTabs> <MTabsItems @bind-Value="_tabIndex"> <MTabItem> <MCard Flat> <MECharts Class="rounded-3" Option="_optionECharts" Height="350"></MECharts> </MCard> </MTabItem> </MTabsItems> </ChildContent> </PDrawer>
页面逻辑代码如下:
using BlazorComponent; using MASA.IoT.Common.Helper; using MASA.IoT.Core.Contract.Device; using MASA.IoT.UI.Caller; using Microsoft.AspNetCore.Components; namespace MASA.IoT.UI.Pages { public partial class DeviceList : ComponentBase { StringNumber _tabIndex; private object _optionECharts = new(); private int _totalCount = 0; private MqttHelper mqttHelper { get; set; } private List<DeviceListViewModel> deviceList { get; set; } = new(); private bool ShowDrawer { get; set; } [Inject] private DeviceCaller _deviceCaller { get; set; } private readonly DeviceListOption _options = new() { PageIndex = 1, PageSize = 10, }; private List<DataTableHeader<DeviceListViewModel>> _headers = new() { new DataTableHeader<DeviceListViewModel> { Text= "设备名称", Align= DataTableHeaderAlign.Start, Sortable= false, Value= nameof(DeviceListViewModel.DeviceName) }, new DataTableHeader<DeviceListViewModel> { Text= "在线状态", Align= DataTableHeaderAlign.Start, Sortable= false, Value= nameof(DeviceListViewModel.OnLineStates) }, new DataTableHeader<DeviceListViewModel> { Text= "Actions", Value= "actions", Sortable=false, Width="100px", Align=DataTableHeaderAlign.Center, } }; private async Task DrawerChangedAsync() { ShowDrawer = !ShowDrawer; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { var paginatedList = await _deviceCaller.DeviceListAsync(new DeviceListOption { PageIndex = 1, PageSize = 10, ProductId = new Guid("C85EF7E5-2E43-4BD2-A939-07FE5EA3F459") }); deviceList = paginatedList.Result.ToList(); _totalCount = (int)paginatedList.Total; StateHasChanged(); } await base.OnAfterRenderAsync(firstRender); } private async Task EditItem(DeviceListViewModel item) { var eChartsData = await _deviceCaller.GetDeviceDataPointList(new GetDeviceDataPointListOption { ProductId = Guid.Parse("c85ef7e5-2e43-4bd2-a939-07fe5ea3f459"), DeviceName = item.DeviceName, StartDateTime = DateTime.Today, StopDateTime = DateTime.Today.AddDays(1) }); if (eChartsData != null) { _optionECharts = GetEChartsData(eChartsData); } ShowDrawer = true; } private async Task TabsValueChanged(StringNumber value) { _tabIndex = value; } private dynamic GetEChartsData(EChartsData data) { return new { tooltip = new { trigger = "axis" }, legend = new { Data = new[] { "Pm2.5", "Humidity", "Temperature" } }, XAxis = new { Type = "category", Data = data.FieldDataList.First().DateTimes.Select(o => o.ToLocalTime().ToString("t")) }, YAxis = new { Min = 10, Max = 100, Type = "value", }, Series = new[] { new { Name ="Pm2.5", Type = "line", Smooth = true, Data = data.FieldDataList.First(o => o.FieldName=="PM_25").Values }, new { Name ="Humidity", Type = "line", Smooth = true, Data = data.FieldDataList.First(o => o.FieldName=="Humidity").Values }, new { Name ="Temperature", Type = "line", Smooth = true, Data = data.FieldDataList.First(o => o.FieldName=="Temperature").Values } } }; } } }
这里查询当天数据(StartDateTime = DateTime.Today, StopDateTime = DateTime.Today.AddDays(1)),GetEChartsData方法返回匿名对象用于ECharts展示,其他内容相对简单不过多赘述。
效果
效果如下
总结
influxdb的自带统计函数很多,可以满足业务上的绝大多数需求,而且还可以自定义函数,结合MASA Blazor和ECharts可以轻松打造丰富直观的Dashboard。另外Influxdb的UI界面也支持定义Dashboard,目前支持八种图表展示。
完整代码在这里:https://github.com/sunday866/MASA.IoT-Training-Demos
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具