You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
350 lines
10 KiB
JavaScript
350 lines
10 KiB
JavaScript
const { query, queryOne } = require('../../db');
|
|
|
|
const METRIC_FIELD_MAP = {
|
|
'正向有功总电能': 'forward_active_total_energy',
|
|
'正向无功总电能': 'forward_reactive_total_energy',
|
|
'总有功功率': 'total_active_power',
|
|
'总无功功率': 'total_reactive_power',
|
|
A相电压: 'phase_a_voltage',
|
|
B相电压: 'phase_b_voltage',
|
|
C相电压: 'phase_c_voltage',
|
|
A相电流: 'phase_a_current',
|
|
B相电流: 'phase_b_current',
|
|
C相电流: 'phase_c_current',
|
|
总功率因数: 'total_power_factor',
|
|
水表读数: 'water_meter_reading',
|
|
用水量: 'water_meter_reading',
|
|
温度读数: 'temperature_reading',
|
|
平均温度: 'temperature_reading',
|
|
最高温度: 'temperature_reading',
|
|
最低温度: 'temperature_reading'
|
|
};
|
|
|
|
const AGGREGATION_SQL = {
|
|
avg: (field) => `avg(${field})`,
|
|
sum: (field) => `sum(${field})`,
|
|
max: (field) => `max(${field})`,
|
|
min: (field) => `min(${field})`
|
|
};
|
|
|
|
const AGGREGATION_RULES = new Set([...Object.keys(AGGREGATION_SQL), 'subtract']);
|
|
const COLLECTION_FIELDS = new Set(Object.values(METRIC_FIELD_MAP));
|
|
const LINE_INTERVAL_SECONDS = new Set([5, 10, 15, 30, 60, 300, 600, 1800, 3600, 86400]);
|
|
|
|
function splitIds(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => String(item).trim()).filter(Boolean);
|
|
}
|
|
return String(value || '')
|
|
.split(',')
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function pad(value) {
|
|
return String(value).padStart(2, '0');
|
|
}
|
|
|
|
function formatDateTime(date) {
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
}
|
|
|
|
function normalizeDateTime(value, fallbackDate) {
|
|
if (!value) {
|
|
return formatDateTime(fallbackDate);
|
|
}
|
|
const text = String(value).trim();
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
return `${text} 00:00:00`;
|
|
}
|
|
if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(text)) {
|
|
return `${text}:00`;
|
|
}
|
|
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(text)) {
|
|
return `${text.replace('T', ' ')}:00`;
|
|
}
|
|
return text.replace('T', ' ').slice(0, 19);
|
|
}
|
|
|
|
function defaultRange() {
|
|
const end = new Date();
|
|
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
|
|
return {
|
|
beginTime: formatDateTime(start),
|
|
endTime: formatDateTime(end)
|
|
};
|
|
}
|
|
|
|
function ok(data) {
|
|
return {
|
|
code: 200,
|
|
msg: '查询成功',
|
|
data
|
|
};
|
|
}
|
|
|
|
function emptyLine(beginTime, endTime, metric, interval) {
|
|
return ok({
|
|
xAxis: [],
|
|
series: [],
|
|
devices: [],
|
|
records: [],
|
|
metric,
|
|
beginTime,
|
|
endTime,
|
|
interval
|
|
});
|
|
}
|
|
|
|
async function getMetric(config, metricId) {
|
|
const metric = metricId
|
|
? await queryOne(
|
|
`select id, type, unit, metric_name, collection_field, aggregation_rule
|
|
from energy_metric_library
|
|
where id = :metricId
|
|
and type = :deviceType
|
|
limit 1`,
|
|
{ metricId, deviceType: String(config.deviceType) }
|
|
)
|
|
: null;
|
|
|
|
if (metric) {
|
|
return {
|
|
id: metric.id,
|
|
type: String(metric.type),
|
|
name: metric.metric_name,
|
|
unit: metric.unit || '',
|
|
aggregationRule: metric.aggregation_rule || config.defaultAggregationRule,
|
|
field: COLLECTION_FIELDS.has(metric.collection_field) ? metric.collection_field : METRIC_FIELD_MAP[metric.metric_name]
|
|
};
|
|
}
|
|
|
|
return {
|
|
id: '',
|
|
type: String(config.deviceType),
|
|
name: config.defaultMetricName,
|
|
unit: config.defaultUnit || '',
|
|
aggregationRule: config.defaultAggregationRule,
|
|
field: config.defaultField
|
|
};
|
|
}
|
|
|
|
function normalizeAggregationRule(rule) {
|
|
return AGGREGATION_RULES.has(rule) ? rule : 'avg';
|
|
}
|
|
|
|
function normalizeInterval(value) {
|
|
if (value === null || value === undefined || value === '') {
|
|
return undefined;
|
|
}
|
|
const interval = Number(value);
|
|
return LINE_INTERVAL_SECONDS.has(interval) ? interval : undefined;
|
|
}
|
|
|
|
function buildReportTimeSql(interval) {
|
|
if (!interval) {
|
|
return "date_format(report_time, '%Y-%m-%d %H:%i:%s')";
|
|
}
|
|
return `date_format(from_unixtime(floor(unix_timestamp(report_time) / ${interval}) * ${interval}), '%Y-%m-%d %H:%i:%s')`;
|
|
}
|
|
|
|
function buildLocationMaps(rows) {
|
|
const byId = new Map(rows.map((row) => [String(row.id), row]));
|
|
const nameMap = new Map();
|
|
const locationMap = new Map();
|
|
|
|
function pathOf(row) {
|
|
const names = [];
|
|
let current = row;
|
|
const visited = new Set();
|
|
while (current && !visited.has(String(current.id))) {
|
|
visited.add(String(current.id));
|
|
if (current.name && current.isDevice !== '1' && current.isDeviceGroup !== '1') {
|
|
names.unshift(current.name);
|
|
}
|
|
current = byId.get(String(current.pid));
|
|
}
|
|
return names.join(' / ');
|
|
}
|
|
|
|
rows.forEach((row) => {
|
|
if (row.deviceId) {
|
|
nameMap.set(row.deviceId, row.name || row.deviceId);
|
|
locationMap.set(row.deviceId, pathOf(row));
|
|
}
|
|
});
|
|
|
|
return { nameMap, locationMap };
|
|
}
|
|
|
|
function createMetricLineService(config) {
|
|
async function devices() {
|
|
const rows = await query(
|
|
`select device_id as deviceId,
|
|
name
|
|
from \`floorInfo\`
|
|
where del_flag = '0'
|
|
and is_device = '1'
|
|
and device_type = :deviceType
|
|
and device_id is not null
|
|
and device_id <> ''
|
|
order by create_time asc, id asc`,
|
|
{ deviceType: String(config.deviceType) }
|
|
);
|
|
|
|
return ok(rows.map((row) => ({
|
|
label: row.name || row.deviceId,
|
|
value: row.deviceId
|
|
})));
|
|
}
|
|
|
|
async function line(queryParams = {}) {
|
|
const range = defaultRange();
|
|
const beginTime = normalizeDateTime(queryParams.beginTime, new Date(range.beginTime));
|
|
const endTime = normalizeDateTime(queryParams.endTime, new Date(range.endTime));
|
|
const deviceIds = splitIds(queryParams.deviceIds);
|
|
const metric = await getMetric(config, queryParams.metricId);
|
|
const interval = normalizeInterval(queryParams.interval);
|
|
|
|
if (!metric.field || !deviceIds.length) {
|
|
return emptyLine(beginTime, endTime, metric, interval);
|
|
}
|
|
|
|
const aggregationRule = normalizeAggregationRule(metric.aggregationRule);
|
|
const reportTimeSql = buildReportTimeSql(interval);
|
|
const params = { beginTime, endTime };
|
|
const placeholders = deviceIds.map((deviceId, index) => {
|
|
params[`deviceId${index}`] = deviceId;
|
|
return `:deviceId${index}`;
|
|
});
|
|
|
|
const nodeRows = await query(
|
|
`select id,
|
|
pid,
|
|
name,
|
|
is_device as isDevice,
|
|
is_device_group as isDeviceGroup,
|
|
device_id as deviceId
|
|
from \`floorInfo\`
|
|
where del_flag = '0'
|
|
order by create_time asc, id asc`
|
|
);
|
|
const { nameMap, locationMap } = buildLocationMaps(nodeRows);
|
|
|
|
let rows;
|
|
if (!interval) {
|
|
rows = await query(
|
|
`select device_id as deviceId,
|
|
${reportTimeSql} as reportTime,
|
|
${metric.field} as value
|
|
from device_collection_data_info
|
|
where report_time >= :beginTime
|
|
and report_time <= :endTime
|
|
and ${metric.field} is not null
|
|
and device_id in (${placeholders.join(', ')})
|
|
order by report_time asc, id asc, device_id asc`,
|
|
params
|
|
);
|
|
} else if (aggregationRule === 'subtract') {
|
|
rows = await query(
|
|
`select deviceId,
|
|
reportTime,
|
|
max(case when rnDesc = 1 then metricValue end) - max(case when rnAsc = 1 then metricValue end) as value
|
|
from (
|
|
select device_id as deviceId,
|
|
${reportTimeSql} as reportTime,
|
|
${metric.field} as metricValue,
|
|
row_number() over (
|
|
partition by device_id, ${reportTimeSql}
|
|
order by report_time asc, id asc
|
|
) as rnAsc,
|
|
row_number() over (
|
|
partition by device_id, ${reportTimeSql}
|
|
order by report_time desc, id desc
|
|
) as rnDesc
|
|
from device_collection_data_info
|
|
where report_time >= :beginTime
|
|
and report_time <= :endTime
|
|
and ${metric.field} is not null
|
|
and device_id in (${placeholders.join(', ')})
|
|
) source
|
|
group by deviceId, reportTime
|
|
order by reportTime asc, deviceId asc`,
|
|
params
|
|
);
|
|
} else {
|
|
rows = await query(
|
|
`select device_id as deviceId,
|
|
${reportTimeSql} as reportTime,
|
|
${AGGREGATION_SQL[aggregationRule](metric.field)} as value
|
|
from device_collection_data_info
|
|
where report_time >= :beginTime
|
|
and report_time <= :endTime
|
|
and ${metric.field} is not null
|
|
and device_id in (${placeholders.join(', ')})
|
|
group by device_id, reportTime
|
|
order by reportTime asc, device_id asc`,
|
|
params
|
|
);
|
|
}
|
|
|
|
const xAxis = [...new Set(rows.map((row) => row.reportTime))];
|
|
const byDevice = new Map();
|
|
rows.forEach((row) => {
|
|
if (!byDevice.has(row.deviceId)) {
|
|
byDevice.set(row.deviceId, new Map());
|
|
}
|
|
byDevice.get(row.deviceId).set(row.reportTime, Number(row.value));
|
|
});
|
|
|
|
const series = deviceIds.map((deviceId) => {
|
|
const values = byDevice.get(deviceId) || new Map();
|
|
return {
|
|
deviceId,
|
|
name: nameMap.get(deviceId) || deviceId,
|
|
type: 'line',
|
|
smooth: true,
|
|
connectNulls: true,
|
|
data: xAxis.map((time) => values.get(time) ?? null)
|
|
};
|
|
});
|
|
const records = rows.map((row) => ({
|
|
deviceId: row.deviceId,
|
|
deviceName: nameMap.get(row.deviceId) || row.deviceId,
|
|
deviceLocation: locationMap.get(row.deviceId) || '',
|
|
reportTime: row.reportTime,
|
|
metricName: metric.name,
|
|
metricUnit: metric.unit,
|
|
value: row.value === null || row.value === undefined ? null : Number(row.value)
|
|
}));
|
|
|
|
return ok({
|
|
xAxis,
|
|
series,
|
|
devices: deviceIds.map((deviceId) => ({
|
|
deviceId,
|
|
name: nameMap.get(deviceId) || deviceId
|
|
})),
|
|
metric: {
|
|
id: metric.id,
|
|
name: metric.name,
|
|
unit: metric.unit,
|
|
aggregationRule
|
|
},
|
|
records,
|
|
beginTime,
|
|
endTime,
|
|
interval
|
|
});
|
|
}
|
|
|
|
return {
|
|
devices,
|
|
line
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
createMetricLineService
|
|
};
|