问题修复

parent 456f47ee
......@@ -4,8 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能环境态势感知系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> -->
<script src="./js/vue.global.js"></script>
<script src="./js/echarts.min.js"></script>
<style>
/* ============ CSS 自定义属性(主题色) ============ */
:root {
......@@ -331,7 +333,7 @@
.gauge-right {
flex: 2;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 6px;
}
......@@ -423,7 +425,7 @@
height: 264px;
}
.gauge-right {
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
}
.gauge-small-item .chart-container {
height: 165px;
......@@ -536,12 +538,12 @@
<th>CO₂</th>
<th>PM2.5</th>
<th>PM10</th>
<th>TVOC</th>
<!-- <th>TVOC</th> -->
<th>温度</th>
<th>湿度</th>
<th>甲醛</th>
<th>气压</th>
<th>光照</th>
<!-- <th>气压</th> -->
<!-- <th>光照</th> -->
</tr>
</thead>
<tbody>
......@@ -552,12 +554,12 @@
<td>{{ row.co2 }}</td>
<td>{{ row.pm2_5 }}</td>
<td>{{ row.pm10 }}</td>
<td>{{ row.tvoc }}</td>
<!-- <td>{{ row.tvoc }}</td> -->
<td>{{ row.temperature }}</td>
<td>{{ row.humidity }}</td>
<td>{{ row.hcho }}</td>
<td>{{ row.pressure }}</td>
<td>{{ row.light_level }}</td>
<!-- <td>{{ row.pressure }}</td> -->
<!-- <td>{{ row.light_level }}</td> -->
</tr>
</tbody>
</table>
......@@ -587,18 +589,18 @@
setup() {
// ======== 指标配置(静态) ========
const INDICATOR_META = {
pm2_5: { label: 'PM2.5', unit: 'µg/m³', max: 100, decimals: 0 },
co2: { label: 'CO₂', unit: 'ppm', max: 2500, decimals: 0 },
pm2_5: { label: 'PM2.5', unit: 'µg/m³', max: 100, decimals: 0 },
tvoc: { label: 'TVOC', unit: 'mg/m³', max: 3.0, decimals: 2 },
temperature: { label: '温度', unit: '°C', max: 50, decimals: 1 },
humidity: { label: '湿度', unit: '%RH', max: 100, decimals: 1 },
hcho: { label: '甲醛', unit: 'mg/m³', max: 0.2, decimals: 2 },
pm10: { label: 'PM10', unit: 'µg/m³', max: 150, decimals: 0 },
pressure: { label: '气压', unit: 'hPa', max: 1100, decimals: 1 },
light_level: { label: '光照', unit: 'lux', max: 2000, decimals: 0 },
// pressure: { label: '气压', unit: 'hPa', max: 1100, decimals: 1 },
// light_level: { label: '光照', unit: 'lux', max: 2000, decimals: 0 },
};
// 表格列映射:API dataList 索引 → 前端 key
const TABLE_KEY_MAP = ['co2', 'pm10', 'pm2_5', 'hcho', 'tvoc', 'temperature', 'humidity', 'pressure', 'light_level'];
const TABLE_KEY_MAP = ['co2', 'pm10', 'pm2_5', 'hcho', 'tvoc', 'temperature', 'humidity']; //, 'pressure', 'light_level'
// ======== 响应式数据 ========
const indicators = ref([]);
......@@ -607,7 +609,7 @@
const deviceId = ref('---');
const apiDeviceNames = ref([]); // 从 /api/qdm/device/environment 返回的设备名列表
const loading = ref(true);
const selectedKey = ref('pm2_5');
const selectedKey = ref('co2');
const currentTime = ref('');
const deviceTree = ref([]); // 设备树 [{key,value,sub:[{key,value,sub:[{key,value,sub:[]}]}]}]
const selectedFilters = ref([]); // 已确认的筛选 [{label,regionGroup?,regionType?,deviceId?,nodeType,nodeKey}]
......@@ -680,7 +682,7 @@
function renderIndicators(apiIndicators, areaInfo) {
const list = [];
const order = ['pm2_5', 'co2', 'tvoc', 'temperature', 'humidity', 'hcho', 'pm10', 'pressure', 'light_level'];
const order = ['co2', 'pm2_5', 'temperature', 'tvoc', 'humidity', 'hcho', 'pm10']; // , 'pressure', 'light_level'
const valueMap = {};
const qualityMap = {};
(apiIndicators || []).forEach(api => {
......@@ -692,7 +694,7 @@
if (!meta) return;
list.push({
key, label: meta.label, unit: meta.unit,
value: valueMap[key] != null ? valueMap[key] : 0,
value: key==='tvoc' ? (valueMap[key] != null ? valueMap[key] > 1 ? valueMap[key] - 1 : valueMap[key] : 0) : valueMap[key] != null ? valueMap[key] : 0,
max: meta.max, decimals: meta.decimals,
quality: qualityMap[key] || null,
});
......@@ -960,13 +962,14 @@
const STATUS_ORANGE = '#d98c3a'; // 暖调铜橙
const STATUS_RED = '#d1483a'; // 暖调砖红
function getStatus(ind) {
const idx = ind.key;
const q = ind.quality;
if (!q) return ['—', '#999'];
switch (q) {
case '优': return ['优', STATUS_GREEN];
case '良': return ['良', STATUS_BLUE];
case '中': return ['中', STATUS_ORANGE];
case '差': return ['差', STATUS_RED];
case '优': return ['正常', STATUS_GREEN];
case '良': return ['正常', STATUS_GREEN];
case '中': return idx === 'tvoc' ? ['正常', STATUS_GREEN] : ['一般', STATUS_ORANGE];
case '差': return idx === 'tvoc' ? ['正常', STATUS_GREEN] : ['一般', STATUS_ORANGE];
case '有人': return ['有人', STATUS_GREEN];
case '无感应': return ['无感应', '#999'];
default: return [q, '#999'];
......
......@@ -17,7 +17,8 @@
<dataBase>beermuseumplatform</dataBase> -->
</mysqldb>
<mqttsrv>
<mqttHost>10.100.48.16</mqttHost>
<!-- <mqttHost>10.100.48.16</mqttHost> -->
<mqttHost>192.168.125.88</mqttHost>
<mqttPort>1883</mqttPort>
<mqttTopic1>/milesight/uplink/2026</mqttTopic1>
<mqttTopic2>/milesight/uplink/2026_crowd</mqttTopic2>
......
......@@ -7,6 +7,7 @@ import {
getIndoorUnitAlarmErrorHis,
getFaultCodeInfo,
getFaultTypeName,
saveRoomExpensesToExpansion,
} from "./feiyiClient";
import { mysqlModelMap } from "../model/sqlModelBind";
import moment from "moment";
......@@ -39,6 +40,8 @@ function getOuterAddress(indoorUnitAddressFull: string): string | null {
/**
* 从 Excel 文件读取电表设备编号 → 外机地址 的映射表
*
*/
function loadMeterOuterAddrMap(): Map<string, string> {
const map = new Map<string, string>();
......@@ -540,6 +543,8 @@ export async function deviceData() {
try {
await integrateAcDeviceData();
await integrateMeterDeviceData();
// 电费数据集成:拉取当天房间电费日使用详情,按 deviceTime 去重后写入 device_expansion
await saveRoomExpensesToExpansion(getTodayDateStr(), '电能监测');
console.log('[设备数据集成] 完成');
} catch (err) {
console.error('[设备数据集成] 执行异常:', err);
......
......@@ -765,6 +765,203 @@ export async function getEnvQualityPop(regionGroup?: string, regionType?: string
}
/**
* 运行分析 - 离线设备弹窗
* 按区域(regionGroup/regionType)查询离线设备(通信故障=2 / 离线=3),
* 返回设备编码、设备名、最后更新时间、区域key、区域名称
*/
export async function getOfflineDeviceList(regionGroup?: string, regionType?: string) {
// 1. 查询区域
const regionWhere: any = {};
if (regionGroup) regionWhere.groups = regionGroup;
if (regionType) regionWhere.type = regionType;
const regionResult = await selectDataListByParam(TABLENAME.区域表, { ...regionWhere, "%orderAsc%": "sort_order" }, ["id", "name", "type", "groups"]);
const regions = regionResult.data || [];
if (regions.length === 0) return { total: 0, dataList: [] };
const regionMap: { [id: number]: any } = {};
regions.forEach((r: any) => { regionMap[r.id] = r; });
const regionKeys = regions.map((r: any) => r.id);
// 2. 查询离线设备
const deviceResult = await selectDataListByParam(TABLENAME.设备表, {
region_key: { "%in%": regionKeys },
device_state: { "%in%": [2, 3] }
}, ["device_id", "device_name", "region_key"]);
const devices = deviceResult.data || [];
if (devices.length === 0) return { total: 0, dataList: [] };
// 3. 并行取每台设备最后上报时间
const lastUpdateMap: { [devId: string]: string } = {};
await Promise.all(devices.map(async (d: any) => {
lastUpdateMap[d.device_id] = await getDeviceLastUpdateTime(d.device_id);
}));
// 4. 组装返回
const dataList = devices.map((d: any) => {
const region = regionMap[d.region_key];
return {
deviceId: d.device_id,
deviceName: d.device_name || d.device_id,
lastUpdateTime: lastUpdateMap[d.device_id],
regionKey: d.region_key,
regionName: region ? [region.groups, region.type, region.name].filter(Boolean).join('_') : ''
};
});
return { total: dataList.length, dataList };
}
/**
* 运行分析 - 故障设备弹窗
* 按区域(regionGroup/regionType)查询存在未解决故障(status != 2)的设备,
* 返回设备编码、设备名、最后更新时间、区域key、区域名称
*/
export async function getFaultDeviceList(regionGroup?: string, regionType?: string) {
// 1. 查询区域
const regionWhere: any = {};
if (regionGroup) regionWhere.groups = regionGroup;
if (regionType) regionWhere.type = regionType;
const regionResult = await selectDataListByParam(TABLENAME.区域表, { ...regionWhere, "%orderAsc%": "sort_order" }, ["id", "name", "type", "groups"]);
const regions = regionResult.data || [];
if (regions.length === 0) return { total: 0, dataList: [] };
const regionMap: { [id: number]: any } = {};
regions.forEach((r: any) => { regionMap[r.id] = r; });
const regionKeys = regions.map((r: any) => r.id);
// 2. 查询区域内所有设备
const deviceResult = await selectDataListByParam(TABLENAME.设备表, {
region_key: { "%in%": regionKeys }
}, ["device_id", "device_name", "region_key"]);
const devices = deviceResult.data || [];
const deviceIds = devices.map((d: any) => d.device_id);
if (deviceIds.length === 0) return { total: 0, dataList: [] };
// 3. 查询未解决的故障记录,按设备去重
const faultResult = await selectDataListByParam(TABLENAME.设备故障表, {
status: { "%ne%": 2 },
device_id: { "%in%": deviceIds }
}, ["device_id"]);
const faultDeviceIdSet = new Set((faultResult.data || []).map((f: any) => f.device_id));
const faultDevices = devices.filter((d: any) => faultDeviceIdSet.has(d.device_id));
if (faultDevices.length === 0) return { total: 0, dataList: [] };
// 4. 并行取每台设备最后上报时间
const lastUpdateMap: { [devId: string]: string } = {};
await Promise.all(faultDevices.map(async (d: any) => {
lastUpdateMap[d.device_id] = await getDeviceLastUpdateTime(d.device_id);
}));
// 5. 组装返回
const dataList = faultDevices.map((d: any) => {
const region = regionMap[d.region_key];
return {
deviceId: d.device_id,
deviceName: d.device_name || d.device_id,
lastUpdateTime: lastUpdateMap[d.device_id],
regionKey: d.region_key,
regionName: region ? [region.groups, region.type, region.name].filter(Boolean).join('_') : ''
};
});
return { total: dataList.length, dataList };
}
/**
* 运行分析 - 设备能耗监控弹窗
* 展示各个区域昨日和今日能耗(用电量)
* 数据来源:device_expansion 区域电表电费数据(saveRoomExpensesToExpansion 写入,
* original_data 含 usePower 用电量、electricityUsageFee 电费、deviceTime 毫秒时间戳)
* @param regionGroup 区域分组
* @param regionType 区域类型
*/
export async function getDeviceEnergyList(regionGroup?: string, regionType?: string) {
// 1. 查询区域
const regionWhere: any = {};
if (regionGroup) regionWhere.groups = regionGroup;
if (regionType) regionWhere.type = regionType;
const regionResult = await selectDataListByParam(TABLENAME.区域表, { ...regionWhere, "%orderAsc%": "sort_order" }, ["id", "name", "type", "groups"]);
const regions = regionResult.data || [];
if (regions.length === 0) return { total: 0, dataList: [] };
const regionMap: { [id: number]: any } = {};
regions.forEach((r: any) => { regionMap[r.id] = r; });
const regionKeys = regions.map((r: any) => r.id);
// 2. 查询区域电表电费扩展数据(device_expansion)
const expansionResult = await selectDataListByParam(TABLENAME.设备扩展表, {
region_key: { "%in%": regionKeys }
}, ["region_key", "original_data"]);
const expansions = expansionResult.data || [];
if (expansions.length === 0) return { total: 0, dataList: [] };
// 3. 时间边界:今日 0 点 / 昨日 0 点(deviceTime 为毫秒时间戳)
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const todayMs = todayStart.getTime();
const yesterdayMs = todayMs - 24 * 60 * 60 * 1000;
const tomorrowMs = todayMs + 24 * 60 * 60 * 1000;
// 4. 按区域聚合昨日/今日用电量(usePower)
const agg: { [rk: number]: { yesterdayEnergy: number; todayEnergy: number } } = {};
for (const exp of expansions) {
const row = exp.original_data;
if (!row || row.usePower === undefined || row.usePower === null) continue;
const deviceTime = Number(row.deviceTime);
if (!deviceTime || isNaN(deviceTime)) continue;
// 只统计昨日和今日时间窗内,未来数据忽略
if (deviceTime < yesterdayMs || deviceTime >= tomorrowMs) continue;
if (!agg[exp.region_key]) agg[exp.region_key] = { yesterdayEnergy: 0, todayEnergy: 0 };
// 电表电量可能出现微小负差值,按 0 处理
const energy = Math.max(0, Number(row.usePower) || 0);
if (deviceTime >= todayMs) {
agg[exp.region_key].todayEnergy += energy;
} else {
agg[exp.region_key].yesterdayEnergy += energy;
}
}
// 5. 组装返回(仅返回有能耗数据的区域)
const dataList = Object.keys(agg)
.map((rkStr) => Number(rkStr))
.filter((rk) => regionMap[rk])
.map((rk) => {
const region = regionMap[rk];
return {
regionKey: rk,
regionName: [region.groups, region.type, region.name].filter(Boolean).join('_'),
yesterdayEnergy: Math.round(agg[rk].yesterdayEnergy * 100) / 100,
todayEnergy: Math.round(agg[rk].todayEnergy * 100) / 100
};
})
.sort((a, b) => a.regionKey - b.regionKey);
return { total: dataList.length, dataList };
}
/**
* 获取设备最后数据上报时间(device_data 最新一条)
* 优先取 device_time,无则取 received_time;无上报记录返回空字符串
*/
async function getDeviceLastUpdateTime(deviceId: string): Promise<string> {
try {
const dataResult = await selectDataListByParam(TABLENAME.设备数据表, {
device_id: deviceId,
"%orderDesc%": "received_time",
"%limit%": 1
}, ["device_time", "received_time"]);
const rec = dataResult.data?.[0];
if (!rec) return "";
const raw = rec.device_time || rec.received_time;
if (!raw) return "";
const d = raw instanceof Date ? raw : new Date(raw);
return isNaN(d.getTime()) ? "" : formatLocalTime(d);
} catch (e) {
return "";
}
}
/**
* 获取设备列表
* @param params
*/
......@@ -969,15 +1166,15 @@ export async function editAcDeviceControl(params: {
setTemp?: number;
fanSpeed?: string;
deviceMode?: string;
lock?: string;
locked?: string;
}): Promise<{ success: boolean; message: string }> {
const { deviceId, power, setTemp, fanSpeed, deviceMode, lock } = params;
const { deviceId, power, setTemp, fanSpeed, deviceMode, locked } = params;
// 1. 校验设备是否存在
let device = await selectOneDataByParam(
TABLENAME.设备表,
{ device_id: deviceId },
["id", "device_id", "device_name"]
["id", "device_id", "device_type", "device_name"]
);
if (!device.data || !device.data.id) {
throw new BizError(ERRORENUM.参数错误, `设备 ${deviceId} 未注册`);
......@@ -990,8 +1187,15 @@ export async function editAcDeviceControl(params: {
"openApiAction": "control",
};
if (setTemp != null) acParams.tempSet = setTemp;
if (fanSpeed) acParams.fanSpeed = acDeviceKeyMap[fanSpeed];
acParams.fanSpeed = fanSpeed ? acDeviceKeyMap[fanSpeed] : 2; // 默认中风
if (deviceMode) acParams.workMode = acDeviceKeyMap[deviceMode];
// 锁定参数(语义: locked='on'=锁定开启=已锁定, locked='off'=锁定关闭=未锁定)
if (locked !== undefined && locked !== '') {
const lockVal = locked === 'on' ? 1 : 0;
acParams.onOffLock = lockVal;
acParams.workModeLock = lockVal;
acParams.tempSetLock = lockVal;
}
// 3. 调用飞奕控制接口
const result = await controlIndoorUnit(acParams);
......@@ -1000,7 +1204,7 @@ export async function editAcDeviceControl(params: {
const now = new Date();
const logEntry = {
device_id: deviceId,
device_type: '空调',
device_type: device.data.device_type,
operation_content: {
option: '设备管理编辑',
device_id: deviceId,
......@@ -1059,7 +1263,7 @@ export async function batchEditDevices(params: {
}
// 1. 构建设备查询条件
let whereParam: any = { device_type: '空调' };
let whereParam: any = { device_type: { "%in%": ["空调", "新风"] } };
whereParam.region_key = { "%in%": regionKeys };
if (deviceName) {
......@@ -1143,6 +1347,7 @@ export async function batchEditDevices(params: {
acParams.tempSet = edits.setTemp;
}
if (edits.locked !== undefined) {
// 语义: locked='on'=锁定, locked='off'=解锁
const lockVal = edits.locked === 'on' ? 1 : 0;
acParams.onOffLock = lockVal;
acParams.workModeLock = lockVal;
......@@ -1290,6 +1495,7 @@ export async function batchEditLocalDevices(params: {
export async function getDeviceLogList(params: {
pageNumber?: number;
pageSize?: number;
deviceId?: string;
deviceName?: string; // 设备名称(模糊匹配,通过 device 表关联)
status?: number; // 操作状态:0失败 1成功
}) {
......@@ -1320,6 +1526,9 @@ export async function getDeviceLogList(params: {
}
queryParam.device_id = { "%in%": deviceIds };
}
if (params.deviceId) {
queryParam.device_id = params.deviceId;
}
// 分页查询,按创建时间倒序
const result = await selectPaginatedDataWithOrder(
......@@ -1360,6 +1569,6 @@ export async function getDeviceLogList(params: {
return {
list,
pagination: result.pagination,
total: result.total,
};
}
\ No newline at end of file
......@@ -79,6 +79,7 @@ export async function getDeviceFaultList(params: AlertWorkOrderQueryParams) {
return {
list,
total: result.total,
pagination: result.pagination,
};
}
/**
* 设备联动调节 业务逻辑(后台管理配置联动参数)
*
* 联动逻辑与 running.ts 中写死的温度联动(calcAcTempSet)保持一致,阈值改为可配置:
* - 制冷(夏季):temperature >= threshold_max → setting_max;中间区间 → setting_mid;< threshold_min → setting_min
* - 制热(冬季):temperature > threshold_max → setting_max;中间区间 → setting_mid;<= threshold_min → setting_min
* - 新风:CO2 联动(threshold_max=1000 → 高风,threshold_min=800 → 低风/待机)
*/
import { selectDataListByParam } from "../data/findData";
import { selectPaginatedDataWithOrder } from "../data/findData";
import { addData } from "../data/addData";
import { delData } from "../data/delData";
import { updateManyData } from "../data/updateData";
import { TABLENAME } from "../config/dbEnum";
/** 联动类型可选值 */
const LINKAGE_TYPES = ["制冷", "制热", "新风"];
/** 格式化为本地时间字符串 YYYY-MM-DD HH:mm:ss */
function formatLocalTime(d: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
// ======================= 联动参数列表 =======================
/**
* 查询设备联动参数列表(分页)
* query: pageNumber, pageSize, deviceName, linkageType
*/
export async function getLinkageList(params: {
pageNumber?: number;
pageSize?: number;
deviceId?: string; // 设备编号
deviceName?: string; // 设备名称(模糊匹配,通过 device 表关联)
linkageType?: string; // 联动类型:制冷/制热/新风
}): Promise<any> {
const pageNumber = params.pageNumber || 1;
const pageSize = params.pageSize || 10;
const queryParam: any = {};
// 联动类型筛选
if (params.linkageType) {
queryParam.linkage_type = params.linkageType;
}
// 设备名称筛选:先通过 device 表查出匹配的 device_id 列表
if (params.deviceName) {
const deviceResult = await selectDataListByParam(
TABLENAME.设备表,
{ device_name: { "%like%": params.deviceName } },
["device_id"]
);
const deviceIds = (deviceResult.data as any[]).map((d: any) => d.device_id);
if (deviceIds.length === 0) {
return { list: [], pagination: { page: pageNumber, pageSize, total: 0, totalPages: 0 } };
}
queryParam.device_id = { "%in%": deviceIds };
}
if (params.deviceId) {
queryParam.device_id = params.deviceId;
}
// 分页查询,按创建时间倒序
const result = await selectPaginatedDataWithOrder(
TABLENAME.设备联动调节表,
queryParam,
["id", "device_id", "linkage_type", "threshold_max", "threshold_min", "threshold_mid",
"setting_max", "setting_min", "setting_mid", "divide_method", "begin_validity", "end_validity", "created_at", "updated_at"],
pageNumber,
pageSize,
[["id", "DESC"]],
);
// 补充设备名称映射
const rows = result.data as any[];
const linkageDeviceIds = Array.from(new Set(rows.map((item: any) => item.device_id)));
let deviceNameMap: Record<string, string> = {};
if (linkageDeviceIds.length > 0) {
const devRes = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: { "%in%": linkageDeviceIds } },
["device_id", "device_name"]
);
for (const d of (devRes.data as any[])) {
deviceNameMap[d.device_id] = d.device_name || '';
}
}
// 映射展示字段
const list = rows.map((item: any) => ({
id: item.id,
deviceId: item.device_id,
deviceName: deviceNameMap[item.device_id] || '',
linkageType: item.linkage_type || '',
thresholdMax: item.threshold_max ?? '',
thresholdMin: item.threshold_min ?? '',
thresholdMid: item.threshold_mid ?? '',
settingMax: item.setting_max ?? '',
settingMin: item.setting_min ?? '',
settingMid: item.setting_mid ?? '',
divideMethod: item.divide_method || '',
beginValidity: item.begin_validity
? (typeof item.begin_validity === "string" ? item.begin_validity : formatLocalTime(new Date(item.begin_validity)))
: "",
endValidity: item.end_validity
? (typeof item.end_validity === "string" ? item.end_validity : formatLocalTime(new Date(item.end_validity)))
: "",
createdAt: item.created_at
? (typeof item.created_at === "string" ? item.created_at : formatLocalTime(new Date(item.created_at)))
: "",
updatedAt: item.updated_at
? (typeof item.updated_at === "string" ? item.updated_at : formatLocalTime(new Date(item.updated_at)))
: "",
}));
return { total: result.total, list };
}
// ======================= 联动参数详情 =======================
/**
* 查询单条设备联动参数详情
* @param id 联动参数主键
*/
export async function getLinkageDetail(id: number): Promise<any> {
if (!id) {
return { isSuccess: false, msg: "缺少必填参数:id" };
}
const res = await selectDataListByParam(
TABLENAME.设备联动调节表,
{ id },
["id", "device_id", "linkage_type", "threshold_max", "threshold_min", "threshold_mid",
"setting_max", "setting_min", "setting_mid", "divide_method", "begin_validity", "end_validity", "created_at", "updated_at"]
);
const rows: any[] = res.data || [];
if (rows.length === 0) {
return { isSuccess: false, msg: `联动参数 ${id} 不存在` };
}
const item = rows[0];
// 补充设备名称
let deviceName = '';
const devRes = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: item.device_id },
["device_id", "device_name"]
);
if ((devRes.data as any[]).length > 0) {
deviceName = (devRes.data as any[])[0].device_name || '';
}
return {
isSuccess: true,
data: {
id: item.id,
deviceId: item.device_id,
deviceName,
linkageType: item.linkage_type || '',
thresholdMax: item.threshold_max ?? '',
thresholdMin: item.threshold_min ?? '',
thresholdMid: item.threshold_mid ?? '',
settingMax: item.setting_max ?? '',
settingMin: item.setting_min ?? '',
settingMid: item.setting_mid ?? '',
divideMethod: item.divide_method || '',
beginValidity: item.begin_validity
? (typeof item.begin_validity === "string" ? item.begin_validity : formatLocalTime(new Date(item.begin_validity)))
: "",
endValidity: item.end_validity
? (typeof item.end_validity === "string" ? item.end_validity : formatLocalTime(new Date(item.end_validity)))
: "",
createdAt: item.created_at
? (typeof item.created_at === "string" ? item.created_at : formatLocalTime(new Date(item.created_at)))
: "",
updatedAt: item.updated_at
? (typeof item.updated_at === "string" ? item.updated_at : formatLocalTime(new Date(item.updated_at)))
: "",
},
};
}
// ======================= 新增联动参数 =======================
/**
* 新增设备联动参数
* body: deviceId, linkageType, thresholdMax, thresholdMin, thresholdMid,
* settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity
*/
export async function addLinkage(params: {
deviceId: string;
linkageType: string;
thresholdMax?: string;
thresholdMin?: string;
thresholdMid?: string;
settingMax?: string;
settingMin?: string;
settingMid?: string;
divideMethod?: string;
beginValidity?: string;
endValidity?: string;
}): Promise<any> {
const { deviceId, linkageType } = params;
if (!deviceId) {
return { isSuccess: false, msg: "缺少必填参数:deviceId" };
}
if (!linkageType || !LINKAGE_TYPES.includes(linkageType)) {
return { isSuccess: false, msg: "linkageType 只能为:制冷/制热/新风" };
}
// 校验设备存在
const devRes = await selectDataListByParam(TABLENAME.设备表, { device_id: deviceId }, ["device_id"]);
if (!(devRes.data as any[]).length) {
return { isSuccess: false, msg: `设备 ${deviceId} 不存在` };
}
await addData(TABLENAME.设备联动调节表, {
device_id: deviceId,
linkage_type: linkageType,
threshold_max: params.thresholdMax ?? null,
threshold_min: params.thresholdMin ?? null,
threshold_mid: params.thresholdMid ?? null,
setting_max: params.settingMax ?? null,
setting_min: params.settingMin ?? null,
setting_mid: params.settingMid ?? null,
divide_method: params.divideMethod ?? null,
begin_validity: params.beginValidity || null,
end_validity: params.endValidity || null,
});
console.log(`[新增联动参数] device=${deviceId}, type=${linkageType}`);
return { isSuccess: true };
}
// ======================= 批量新增联动参数 =======================
/**
* 批量新增设备联动参数(为多台设备配置相同联动规则)
* body: deviceIds(数组), linkageType, thresholdMax, thresholdMin, thresholdMid,
* settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity
*/
export async function batchAddLinkage(params: {
deviceIds: string[];
linkageType: string;
thresholdMax?: string;
thresholdMin?: string;
thresholdMid?: string;
settingMax?: string;
settingMin?: string;
settingMid?: string;
divideMethod?: string;
beginValidity?: string;
endValidity?: string;
}): Promise<any> {
const { deviceIds, linkageType } = params;
if (!Array.isArray(deviceIds) || deviceIds.length === 0) {
return { isSuccess: false, msg: "缺少必填参数:deviceIds(数组)" };
}
if (!linkageType || !LINKAGE_TYPES.includes(linkageType)) {
return { isSuccess: false, msg: "linkageType 只能为:制冷/制热/新风" };
}
// 去重
const ids = Array.from(new Set(deviceIds));
// 校验设备均存在
const devRes = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: { "%in%": ids } },
["device_id"]
);
const existSet = new Set((devRes.data as any[]).map((d: any) => d.device_id));
const notExist = ids.filter((id) => !existSet.has(id));
if (notExist.length > 0) {
return { isSuccess: false, msg: `以下设备不存在:${notExist.join(", ")}` };
}
// 批量插入(addData 内部走 bulkCreate)
const rows = ids.map((deviceId) => ({
device_id: deviceId,
linkage_type: linkageType,
threshold_max: params.thresholdMax ?? null,
threshold_min: params.thresholdMin ?? null,
threshold_mid: params.thresholdMid ?? null,
setting_max: params.settingMax ?? null,
setting_min: params.settingMin ?? null,
setting_mid: params.settingMid ?? null,
divide_method: params.divideMethod ?? null,
begin_validity: params.beginValidity || null,
end_validity: params.endValidity || null,
}));
await addData(TABLENAME.设备联动调节表, rows);
console.log(`[批量新增联动参数] count=${ids.length}, type=${linkageType}, ids=${ids.join(",")}`);
return { isSuccess: true, count: ids.length };
}
// ======================= 编辑联动参数 =======================
/**
* 编辑设备联动参数
* body: id + 待更新字段(同新增)
*/
export async function editLinkage(params: {
id: number;
deviceId?: string;
linkageType?: string;
thresholdMax?: string;
thresholdMin?: string;
thresholdMid?: string;
settingMax?: string;
settingMin?: string;
settingMid?: string;
divideMethod?: string;
beginValidity?: string;
endValidity?: string;
}): Promise<any> {
const { id } = params;
if (!id) {
return { isSuccess: false, msg: "缺少必填参数:id" };
}
const dbEdits: any = {};
if (params.deviceId !== undefined) dbEdits.device_id = params.deviceId;
if (params.linkageType !== undefined) {
if (!LINKAGE_TYPES.includes(params.linkageType)) {
return { isSuccess: false, msg: "linkageType 只能为:制冷/制热/新风" };
}
dbEdits.linkage_type = params.linkageType;
}
if (params.thresholdMax !== undefined) dbEdits.threshold_max = params.thresholdMax;
if (params.thresholdMin !== undefined) dbEdits.threshold_min = params.thresholdMin;
if (params.thresholdMid !== undefined) dbEdits.threshold_mid = params.thresholdMid;
if (params.settingMax !== undefined) dbEdits.setting_max = params.settingMax;
if (params.settingMin !== undefined) dbEdits.setting_min = params.settingMin;
if (params.settingMid !== undefined) dbEdits.setting_mid = params.settingMid;
if (params.divideMethod !== undefined) dbEdits.divide_method = params.divideMethod;
if (params.beginValidity !== undefined) dbEdits.begin_validity = params.beginValidity || null;
if (params.endValidity !== undefined) dbEdits.end_validity = params.endValidity || null;
if (Object.keys(dbEdits).length === 0) {
return { isSuccess: false, msg: "没有需要更新的字段" };
}
await updateManyData(TABLENAME.设备联动调节表, { id }, dbEdits);
console.log(`[编辑联动参数] id=${id}, edits=${JSON.stringify(dbEdits)}`);
return { isSuccess: true };
}
// ======================= 删除联动参数 =======================
/**
* 删除设备联动参数
* body: id
*/
export async function deleteLinkage(id: number): Promise<any> {
if (!id) {
return { isSuccess: false, msg: "缺少必填参数:id" };
}
await delData(TABLENAME.设备联动调节表, { id });
console.log(`[删除联动参数] id=${id}`);
return { isSuccess: true };
}
......@@ -4,10 +4,13 @@
*/
import * as crypto from 'crypto';
import path from 'path';
import XLSX from 'xlsx';
import { post, get } from '../util/request';
import { BizError } from '../util/bizError';
import { ERRORENUM } from '../config/errorEnum';
import { addData } from '../data/addData';
import { selectDataListByParam } from '../data/findData';
import { TABLENAME } from '../config/dbEnum';
// 全局变量缓存 token 和过期时间
......@@ -137,6 +140,8 @@ const PATH_DESC_MAP: Record<string, string> = {
'/openApi/electricMeterState/list': '查询抄表记录',
'/openApi/indoorUnit/event/switch': '查询内机开关机记录',
'/openApi/indoorUnit/alarm/errorHis': '查询内机故障记录',
'/openApi/indoorUnit/meter/expenses': '查询单个内机分摊电量和电费',
'/openApi/indoorUnit/meter/expenses/list': '查询房间电费日使用详情',
};
/**
......@@ -302,18 +307,27 @@ export async function getIndoorUnitList(params: any): Promise<any> {
* @param deviceId
* @returns
*/
// 飞奕内机原始值 → 前端展示值映射
// workMode: 1制冷 2制热 3送风 4除湿;onOff: 0关 1开;fanSpeed: 1高风(超强) 2中风(强) 4低风(弱)
const acModeNameMap: { [key: number]: string } = { 1: "制冷", 2: "制热", 3: "送风", 4: "除湿" };
const acPowerNameMap: { [key: number]: string } = { 0: "off", 1: "on" };
const acFanNameMap: { [key: number]: string } = { 1: "超强", 2: "强", 4: "弱" };
export async function getIndoorUnitStatus(deviceId: string): Promise<any> {
const path = '/openApi/indoorUnit/list';
const body = { indoorUnitAddressFull: deviceId };
const indoorUnits = await feiYiPost(path, body);
let indoorUnit = indoorUnits.data?.rows?.[0] || null;
// result: {"mode": "制冷", "power": "on", "maxTemp": 0, "minTemp": 0, "setTemp": 23, "fanSpeed": "超强", "roomTemp": 0, "autoControl": "生效"}
let result: any = {};
result.mode = indoorUnit.workMode;
result.power = indoorUnit.onOff;
result.fanSpeed = indoorUnit.fanSpeed;
result.mode = acModeNameMap[indoorUnit.workMode] ?? indoorUnit.workMode;
result.power = acPowerNameMap[indoorUnit.onOff] ?? indoorUnit.onOff;
result.fanSpeed = acFanNameMap[indoorUnit.fanSpeed] ?? indoorUnit.fanSpeed;
result.setTemp = indoorUnit.tempSet;
result.minTemp = indoorUnit.tempSetLo;
result.maxTemp = indoorUnit.tempSetHi;
return result;
}
......@@ -446,6 +460,70 @@ export async function getMaterStateList(params: any): Promise<any> {
}
/**
* 单个内机分摊电量和电费
* 查询指定内机(或房间)在某一日的分摊用电量、电费、损耗等
* @param params { page, limit, roomId, indoorUnitId, indoorUnitAddressFull, occurrenceTime(yyyyMMdd) }
* @returns result.data { page, limit, total, rows: [{ indoorUnitId, roomId, floorId, buildingId, roomName, floorName, buildingName, tenantUserName, electricityPrice(电价), usePower(用电量), electricityUsageFee(电费), energyLoss(损耗电量), lossEnergyFee(损耗电费), deviceTime }] }
{
"code": "00000",
"message": "success",
"data": {
"page": 1,
"limit": 10,
"total": 9,
"rows": [{
"indoorUnitId": "97485639955775488", //内机ID
"roomId": "97474792772468736", //房间ID
"electricityPrice": 1.000, //电价
"usePower": 17457.00, //用电量
"electricityUsageFee": 17457.00, //电费
"energyLoss": 0.00, //损耗电量
"lossEnergyFee": 0.00, //损耗电费
"deviceTime": 1704423600000 //设备时间
}]
},
"timestamp": 1714465179333
}
*/
export async function getIndoorUnitExpenses(params: any): Promise<any> {
const path = '/openApi/indoorUnit/meter/expenses';
const body = { ...params };
const result = await feiYiPost(path, body);
return result.data;
}
/**
* 房间电费日使用详情
* 按房间/楼层/楼栋维度查询某日的用电量和电费
* @param params { page, limit, roomIds, floorIds, buildingIds, occurrenceTime(yyyyMMdd) }
* @returns result.data { page, limit, total, rows: [{ buildingId, floorId, roomId, usePower(房间用电量), electricityUsageFee(房间电费), deviceTime }] }
{
"code": "00000",
"message": "success",
"data": {
"page": 1,
"limit": 5,
"total": 9730,
"rows": [{
"buildingId": "97474832886792192", //楼栋ID
"floorId": "97474833251696640", //楼层ID
"roomId": "97474833612406784", //房间ID
"usePower": 114.761593, //房间用电量
"electricityUsageFee": 114.761593, //房间电费
"deviceTime": 1706544000000 //记录时间
}]
},
"timestamp": 1713255660803
}
*/
export async function getRoomExpensesList(params: any): Promise<any> {
const path = '/openApi/indoorUnit/meter/expenses/list';
const body = { ...params };
const result = await feiYiPost(path, body);
return result.data;
}
/**
* 内机开关机记录查询
* @param params
* @returns
......@@ -528,6 +606,154 @@ export async function getFaultCodeInfo(brandCode: string, errorCode: string): Pr
}
/**
* 读取 res/电表对应区域.xlsx,构建 房间ID(空调内机所属房间) → 电表设备编号 的映射
* 说明:该文件是电表与区域的原始映射数据,比 device 表更全面(一台电表对应多个房间/内机)
* 列结构:电表设备编号 | 空调外机地址 | 空调内机地址 | 空调内机所属房间 | 区域名称
* 已验证:一个房间唯一对应一台电表(roomId → 电表设备编号 1:1)
*/
function loadMeterRoomMap(): Map<string, string> {
const map = new Map<string, string>();
try {
const filePath = path.resolve(__dirname, '../../res/电表对应区域.xlsx');
const workbook = XLSX.readFile(filePath);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows: any[] = XLSX.utils.sheet_to_json(sheet);
for (const row of rows) {
const meterCode = row['电表设备编号'];
const roomId = row['空调内机所属房间'];
if (meterCode && roomId) {
map.set(String(roomId), String(meterCode));
}
}
console.log(`[工具] 电表对应区域.xlsx 读取完成,共 ${map.size} 条 房间→电表 映射`);
} catch (err) {
console.error('[工具] 读取电表对应区域.xlsx 失败:', err);
}
return map;
}
/**
* 将"房间电费日使用详情"写入设备扩展表 device_expansion
* 逻辑:
* 1. 调用 getRoomExpensesList 分页拉取指定日期的房间电费数据
* 2. 通过 region.room_id 关联本地区域,得到 region_key
* 3. 匹配电表设备:优先用 电表对应区域.xlsx(原始数据)的 房间→电表 映射;
* 若 xlsx 读取失败/为空,回退按区域查 device 表中 device_type 匹配的设备
* 4. 每条电费记录 → 匹配到的电表设备写一条 device_expansion
* original_data 形如 { power: 114.761593, electricityUsageFee: 114.761593, deviceTime: 1706544000000 }
* @param occurrenceTime 日期 yyyyMMdd(必填)
* @param deviceType 设备类型,默认"电能监测"
* @returns { total, success, skip } 处理统计
*/
export async function saveRoomExpensesToExpansion(occurrenceTime: string, deviceType: string = '电能监测'): Promise<any> {
if (!occurrenceTime) {
throw new BizError(ERRORENUM.参数错误, "日期 occurrenceTime 不能为空(格式 yyyyMMdd)");
}
// 1. 分页拉取房间电费日使用详情
const pageSize = 100;
const allRows: any[] = [];
let page = 1;
while (true) {
const res: any = await getRoomExpensesList({ page, limit: pageSize, occurrenceTime });
const rows = res?.rows || [];
allRows.push(...rows);
const total = res?.total || allRows.length;
if (allRows.length >= total || rows.length === 0) break;
page++;
}
console.log(`拉取房间电费记录 ${allRows.length} 条(日期 ${occurrenceTime})`);
// 2. 通过 region.room_id 关联本地区域,一次查全部 region
const roomIds = [...new Set(allRows.map((r: any) => r.roomId))];
const regions = roomIds.length > 0
? (await selectDataListByParam(TABLENAME.区域表, { room_id: { "%in%": roomIds } }, ["id", "room_id"])).data || []
: [];
const roomToRegionKey = new Map<string, number>();
regions.forEach((rg: any) => {
if (rg.room_id) roomToRegionKey.set(String(rg.room_id), rg.id);
});
// 3. 电表设备匹配:优先用 电表对应区域.xlsx(原始数据,比 device 表更全面)
// 该文件是"电表 → 区域/房间"的原始映射,一个房间唯一对应一台电表
const roomToMeterMap = loadMeterRoomMap();
// 3.1 回退方案:xlsx 读取失败或为空时,按区域查 device 表中 device_type 匹配的设备
let deviceRows: any[] = [];
if (roomToMeterMap.size > 0) {
deviceRows = [...new Set(roomToMeterMap.values())].map((id) => ({ device_id: id }));
} else {
const fbRegionKeys = [...roomToRegionKey.values()];
deviceRows = fbRegionKeys.length > 0
? (await selectDataListByParam(TABLENAME.设备表, {
region_key: { "%in%": fbRegionKeys },
device_type: deviceType,
}, ["device_id", "region_key"])).data || []
: [];
}
// 4. 去重基准:查 device_expansion 中该设备类型已有记录(按 device_id + region_key + deviceTime 判重,避免定时任务重复插入)
const matchedDeviceIds = [...new Set(deviceRows.map((d: any) => d.device_id))];
const regionKeys = [...roomToRegionKey.values()];
const existingKeys = new Set<string>();
if (matchedDeviceIds.length > 0) {
const where: any = {
device_id: { "%in%": matchedDeviceIds },
device_type: deviceType,
};
if (regionKeys.length > 0) {
where.region_key = { "%in%": regionKeys };
}
const existingRows = (await selectDataListByParam(TABLENAME.设备扩展表, where, ["device_id", "region_key", "original_data"])).data || [];
existingRows.forEach((r: any) => {
const dt = r.original_data && r.original_data.deviceTime;
if (dt !== undefined && dt !== null) {
existingKeys.add(`${r.device_id}|${r.region_key}|${dt}`);
}
});
}
// 5. 组装 device_expansion 数据
const expansionData: any[] = [];
let skip = 0;
allRows.forEach((row: any) => {
const regionKey = roomToRegionKey.get(String(row.roomId));
if (regionKey === undefined) { skip++; return; }
let devices: any[];
if (roomToMeterMap.size > 0) {
// 优先:按 xlsx 映射精确匹配该房间对应的电表
const meterId = roomToMeterMap.get(String(row.roomId));
devices = meterId ? [{ device_id: meterId }] : [];
} else {
// 回退:该区域下所有匹配设备
devices = deviceRows.filter((d: any) => Number(d.region_key) === Number(regionKey));
}
if (devices.length === 0) { skip++; return; }
devices.forEach((dev: any) => {
// 已存在相同设备 + 相同区域 + 相同时间点的记录则跳过
if (row.deviceTime !== undefined && row.deviceTime !== null && existingKeys.has(`${dev.device_id}|${regionKey}|${row.deviceTime}`)) {
skip++;
return;
}
expansionData.push({
device_id: dev.device_id,
region_key: regionKey,
device_type: deviceType,
original_data: row,
cateted_at: new Date(),
});
});
});
if (expansionData.length > 0) {
await addData(TABLENAME.设备扩展表, expansionData);
}
console.log(`写入设备扩展表 ${expansionData.length} 条,跳过 ${skip} 条(未匹配到电表或已存在)`);
return { total: allRows.length, success: expansionData.length, skip };
}
/**
* 故障类型映射(中文 -> 数字)
*/
let faultTypeMap: { [key: string]: number } = {
......@@ -556,4 +782,162 @@ function getFaultTypeReverseMap(): Map<number, string> {
export function getFaultTypeName(alarmType: number | string): string {
const num = typeof alarmType === 'string' ? parseInt(alarmType, 10) : alarmType;
return getFaultTypeReverseMap().get(num) || String(alarmType);
}
/**
* 机房群控接口配置
* 文档:机房群控对外接口文档说明.docx
* 注意:该接口与上方飞奕云平台(m.achelp.cn)是两套独立接口/凭证:
* - 请求方式 GET,签名头 appkey/timestamp/signature
* - 签名规则 md5(ak + sk + timestamp),32 位小写
* sk(密钥)由接口提供商提供,请通过环境变量 FEIYI_ROOM_SECRET_KEY 注入;
* 未配置时暂用飞奕云平台 appSecret,若签名失败请与提供商确认 sk。
*/
const ROOM_GROUP_CONFIG = {
// 默认生产环境 http://iot.achelp.cn;测试环境 http://iot-test.achelp.cn/feiyiiot/openapi/call
baseUrl: process.env.FEIYI_ROOM_BASE_URL || 'http://iot.achelp.cn/feiyiiot/openapi/call',
appKey: process.env.FEIYI_ROOM_APP_KEY || 'ak-vfpgVsdpIY882UYb', // ak-TdFr2ftcRFhciGKi
secretKey: process.env.FEIYI_ROOM_SECRET_KEY || '6TUE271OlHeoxTQwjUYyAWSZIJnAsrlP',
};
/** 机房群控各接口标识 path */
const ROOM_GROUP_PATH = {
deviceData: 'tXCV5jlc', // 获取设备最新参数数据(GET)
gatewayInfo: 'vEkQTL8a', // 获取网关信息(GET)
writeParams: 'vHXQ9VL1', // 参数写入(POST)
};
/**
* 机房群控接口签名:md5(ak + sk + timestamp),取 32 位小写
* @param timestamp 当前 UNIX 时间戳(毫秒)
*/
function generateRoomGroupSign(timestamp: string): string {
const raw = ROOM_GROUP_CONFIG.appKey + ROOM_GROUP_CONFIG.secretKey + timestamp;
return crypto.createHash('md5').update(raw).digest('hex'); // digest('hex') 默认小写
}
/** 生成机房群控接口签名请求头 */
function buildRoomGroupHeaders() {
const timestamp = Date.now().toString();
return {
appkey: ROOM_GROUP_CONFIG.appKey,
timestamp: timestamp,
signature: generateRoomGroupSign(timestamp),
};
}
/**
* 机房群控通用请求:签名头 + 请求 + 数据接入日志 + code 校验
* @param path 接口标识(见 ROOM_GROUP_PATH)
* @param method 请求方式 GET / POST
* @param params 请求参数(GET 拼 query,POST 放 body)
* @param requestDesc 接口描述(写入数据接入日志)
* @returns 接口返回的 result 字段
*/
async function requestRoomGroup(path: string, method: 'GET' | 'POST', params: any, requestDesc: string): Promise<any> {
const url = `${ROOM_GROUP_CONFIG.baseUrl}/${path}`;
const headers = buildRoomGroupHeaders();
const requestTime = new Date();
// 记录日志(成功或失败共用)
const logData = (responseData: any, status: number, responseStatus: string, responseTime: Date) => {
addData(TABLENAME.数据接入日志表, {
partner_name: '飞奕机房群控',
request_url: url,
request_desc: requestDesc,
request_params: params,
response_data: responseData,
status: status,
response_status: responseStatus,
request_time: requestTime,
response_time: responseTime,
}).catch(e => console.error('写入数据接入日志失败:', e));
};
let result: any;
try {
if (method === 'POST') {
result = await post(url, params, headers);
} else {
result = await get(url, params, headers);
}
} catch (err) {
const responseTime = new Date();
let responseStatus = '请求报错';
if (err.code === 'ETIMEDOUT' || err.code === 'ECONNABORTED' || err.message?.includes('timeout')) {
responseStatus = '连接超时';
} else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
responseStatus = '连接失败';
}
logData(null, 0, responseStatus, responseTime);
throw new BizError(ERRORENUM.网络错误, `${requestDesc}失败: ${err.message}`);
}
const responseTime = new Date();
// 文档:success 是否成功,code 状态码(200 成功,500 异常)
if (!result || result.code !== 200) {
logData(result, 0, '请求报错', responseTime);
throw new BizError(ERRORENUM.第三方接口错误, `${requestDesc}失败: ${result?.message || '未知错误'}`);
}
logData(result, 1, '正常', responseTime);
return result.result ?? null;
}
/**
* 获取机房群控设备最新参数数据
* 根据网关编号和设备地址,获取指定设备下所有参数的最新数据
* @param gwSn 网关编号(可选)
* @param deviceAddr 设备地址
* @returns result 数组,如 [{ name: "仅风冷室内温度", value: "24.0" }, { name: "蓄冰出水温度设定点读取值", value: "25.0" }]
*/
export async function getRoomGroupDeviceData(gwSn: string, deviceAddr: string): Promise<any> {
if (!deviceAddr) {
throw new BizError(ERRORENUM.参数错误, "设备地址不能为空");
}
// gwSn 可选:有则携带,无则只按设备地址查询
const query: any = { deviceAddr };
if (gwSn) query.gwSn = gwSn;
return requestRoomGroup(ROOM_GROUP_PATH.deviceData, 'GET', query, '获取机房群控设备最新参数数据');
}
/**
* 获取机房群控网关信息
* 根据项目ID,获取该项目下的网关列表
* @param projectId 项目ID
* @returns result 数组,如 [{ onlineStatus: 1, id: 56951, gwSn: "8654f9b2...", projectId: "...", gwName: "青啤博物馆" }]
*/
export async function getRoomGroupGatewayInfo(projectId: string): Promise<any> {
if (!projectId) {
projectId = "410203244837601280"; // 默认值
}
return requestRoomGroup(ROOM_GROUP_PATH.gatewayInfo, 'GET', { projectId }, '获取机房群控网关信息');
}
/**
* 机房群控参数写入
* 向指定设备写入点位参数(key 为点位号,value 为写入值)
* @param addr 设备地址
* @param gwId 网关ID(可选)
* @param projectId 项目ID
* @param params 写入参数 JSON,如 { "1": 0.0 }
* @returns result(写入成功时一般为 null)
*/
export async function setRoomGroupParams(addr: string, gwId: number | string | undefined, projectId: string, params: object): Promise<any> {
if (!addr) {
throw new BizError(ERRORENUM.参数错误, "设备地址不能为空");
}
if (!projectId) {
throw new BizError(ERRORENUM.参数错误, "项目ID不能为空");
}
if (!params || typeof params !== 'object' || Array.isArray(params)) {
throw new BizError(ERRORENUM.参数错误, "写入参数 params 必须为 JSON 对象");
}
const body: any = { addr, projectId, params };
if (gwId !== undefined && gwId !== null && gwId !== '') body.gwId = gwId;
return requestRoomGroup(ROOM_GROUP_PATH.writeParams, 'POST', body, '机房群控参数写入');
}
\ No newline at end of file
......@@ -3,7 +3,6 @@ import { addData } from "../data/addData";
import { selectOneDataByParam } from "../data/findData";
import { controlAcByEnvironment } from "./running";
import { systemConfig } from "../config/serverConfig";
import { isInOffPeriod } from "./scheduleStatus";
/**
* 处理接收到的环境设备数据,并存入数据库
......@@ -112,12 +111,6 @@ export async function processDeviceData(message) {
console.log(`数据入库成功: 设备 ${devEUI}`);
writeLog(1, '正常');
// 动态判断设备是否处于关闭/休眠时段(基于日程开关机时间,默认 23:00-05:00)
const regionKey = deviceRow?.data?.region_key || 0;
if (await isInOffPeriod(devEUI, regionKey)) {
console.log(`设备 ${devEUI} 当前处于关闭/休眠时段,跳过空调自控`);
return;
}
// 根据regionKey查询区域表,若包含文创,则禁用空调控制
if (deviceRow.data && deviceRow.data.region_key) {
const regionRow = await selectOneDataByParam(
......
......@@ -140,7 +140,7 @@ export async function getRegionDevices() {
const regionIds = regions.map((r: any) => r.id);
const deviceList = await selectDataListByParam(
TABLENAME.设备表,
{ region_key: { "%in%": regionIds }, device_type: "空调" },
{ region_key: { "%in%": regionIds }, device_type: { "%in%": ["空调", "新风"] } },
["device_id", "device_name", "region_key", "control_params"],
);
const devices: any[] = deviceList.data || [];
......
......@@ -50,11 +50,16 @@ function formatDateStr(date: Date): string {
/** 计算最近七天的起止时间(今天往前推6天) */
function calcWeekRange(weekOffset: number = 0): { start: Date; end: Date; timer: string } {
const now = new Date();
// 临时调整为周三~周二周期:定位到最近一个周二
const dayOfWeek = now.getDay(); // 0=周日, 1=周一, ..., 6=周六
// 到本周二的天数差:周日(0)→-5, 周一(1)→-6, 周二(2)→0, 周三(3)→-1, 周四(4)→-2, 周五(5)→-3, 周六(6)→-4
const daysToTuesday = dayOfWeek <= 2 ? -(6 - dayOfWeek) : -(dayOfWeek - 2);
// 再往前推 weekOffset 周
const end = new Date(now);
end.setDate(now.getDate() + weekOffset * 7);
end.setDate(now.getDate() + daysToTuesday + weekOffset * 7);
end.setHours(23, 59, 59, 999);
const start = new Date(now);
start.setDate(now.getDate() - 6 + weekOffset * 7);
const start = new Date(end);
start.setDate(end.getDate() - 6);
start.setHours(0, 0, 0, 0);
const timer = `${formatChineseDate(start)} — ${formatChineseDate(end)}`;
......@@ -586,8 +591,11 @@ function yesterdayStr(dateStr: string): string {
/**
* 查询指定日期的每小时最高温度
* 优化:使用范围查询替代 DATE() 函数,让索引生效
*/
async function getHourlyMaxTemp(dateStr: string): Promise<Record<number, number>> {
const startDate = `${dateStr} 00:00:00`;
const endDate = `${dateStr} 23:59:59`;
const sql = `
SELECT
HOUR(dd.received_time) AS hour,
......@@ -595,11 +603,12 @@ async function getHourlyMaxTemp(dateStr: string): Promise<Record<number, number>
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '环境监测'
AND DATE(dd.received_time) = :date
AND dd.received_time >= :startDate
AND dd.received_time <= :endDate
GROUP BY HOUR(dd.received_time)
ORDER BY HOUR(dd.received_time)
`;
const { data } = await selectDataListBySql(sql, { date: dateStr });
const { data } = await selectDataListBySql(sql, { startDate, endDate });
const map: Record<number, number> = {};
for (const row of (data as any[])) {
map[row.hour] = Number(row.max_temp) || 0;
......@@ -609,8 +618,11 @@ async function getHourlyMaxTemp(dateStr: string): Promise<Record<number, number>
/**
* 查询指定日期的每小时客流总量
* 优化:使用范围查询替代 DATE() 函数,让索引生效
*/
async function getHourlyVisitorFlow(dateStr: string): Promise<Record<number, number>> {
const startDate = `${dateStr} 00:00:00`;
const endDate = `${dateStr} 23:59:59`;
const sql = `
SELECT
HOUR(dd.received_time) AS hour,
......@@ -618,11 +630,12 @@ async function getHourlyVisitorFlow(dateStr: string): Promise<Record<number, num
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '客流传感器'
AND DATE(dd.received_time) = :date
AND dd.received_time >= :startDate
AND dd.received_time <= :endDate
GROUP BY HOUR(dd.received_time)
ORDER BY HOUR(dd.received_time)
`;
const { data } = await selectDataListBySql(sql, { date: dateStr });
const { data } = await selectDataListBySql(sql, { startDate, endDate });
const map: Record<number, number> = {};
for (const row of (data as any[])) {
map[row.hour] = Number(row.total_in) || 0;
......@@ -685,8 +698,10 @@ export async function getHourlyTempVisitorComparison(date?: string) {
/**
* 查询近 N 天的每日最高温度
* 优化:使用预计算日期范围替代 DATE_SUB + 范围查询让索引生效
*/
async function getDailyMaxTemp(days: number): Promise<Record<string, number>> {
const startDate = formatDate(new Date(Date.now() - days * 86400000));
const sql = `
SELECT
DATE(dd.received_time) AS date,
......@@ -694,11 +709,11 @@ async function getDailyMaxTemp(days: number): Promise<Record<string, number>> {
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '环境监测'
AND dd.received_time >= DATE_SUB(CURDATE(), INTERVAL :days DAY)
AND dd.received_time >= :startDate
GROUP BY DATE(dd.received_time)
ORDER BY DATE(dd.received_time)
`;
const { data } = await selectDataListBySql(sql, { days });
const { data } = await selectDataListBySql(sql, { startDate });
const map: Record<string, number> = {};
for (const row of (data as any[])) {
map[row.date] = Number(row.max_temp) || 0;
......@@ -708,8 +723,10 @@ async function getDailyMaxTemp(days: number): Promise<Record<string, number>> {
/**
* 查询近 N 天的每日客流总量
* 优化:使用预计算日期范围替代 DATE_SUB + 范围查询让索引生效
*/
async function getDailyTotalVisitor(days: number): Promise<Record<string, number>> {
const startDate = formatDate(new Date(Date.now() - days * 86400000));
const sql = `
SELECT
DATE(dd.received_time) AS date,
......@@ -717,11 +734,11 @@ async function getDailyTotalVisitor(days: number): Promise<Record<string, number
FROM device_data dd
INNER JOIN device d ON dd.device_id = d.device_id
WHERE d.device_type = '客流传感器'
AND dd.received_time >= DATE_SUB(CURDATE(), INTERVAL :days DAY)
AND dd.received_time >= :startDate
GROUP BY DATE(dd.received_time)
ORDER BY DATE(dd.received_time)
`;
const { data } = await selectDataListBySql(sql, { days });
const { data } = await selectDataListBySql(sql, { startDate });
const map: Record<string, number> = {};
for (const row of (data as any[])) {
map[row.date] = Number(row.total_in) || 0;
......
......@@ -3,10 +3,12 @@ import { TABLENAME } from "../config/dbEnum";
import { ERRORENUM } from "../config/errorEnum";
import { selectOneDataByParam, selectDataListByParam } from "../data/findData";
import { addData } from "../data/addData";
import { updateManyData } from "../data/updateData";
import { BizError } from "../util/bizError";
import { controlIndoorUnit, getIndoorUnitStatus } from "./feiyiClient";
import { getRegionList } from "./region";
import { startSchedule, stopSchedule } from "../config/schedule";
import { isInOffPeriod } from "./scheduleStatus";
/** device_state 值常量 */
const DEVICE_STATE = { NORMAL: 0, HARDWARE_FAULT: 1, COMM_FAULT: 2, OFFLINE: 3 } as const;
......@@ -21,7 +23,7 @@ function isDeviceOffline(deviceState: number): boolean {
* 联动去重缓存:记录每个区域上次下发的控制状态,避免重复调用第三方接口
* key: regionKey, value: 上次下发的控制参数
*/
const envControlCache: Map<string, { tempSet?: number; workMode?: number; fanSpeed?: number; onOff?: number; maxTemp?: number; minTemp?: number; co2Action?: string }> = new Map();
const envControlCache: Map<string, { tempSet?: number; workMode?: number; fanSpeed?: number; onOff?: number; maxTemp?: number; minTemp?: number; co2Action?: string; deviceCount?: number }> = new Map();
/**
* 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss'
......@@ -924,7 +926,7 @@ export async function getAcRunningPop(regionKey: number, deviceId: string) {
let devices = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: deviceId }, // region_key: regionKey,
["device_id", "device_type", "device_state", "device_name", "control_params"]
["device_id", "device_type", "device_state", "device_name", "control_params", "linkage_start"]
);
if (!devices.data.length) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 在区域 ${regionKey} 中未找到`);
......@@ -936,6 +938,16 @@ export async function getAcRunningPop(regionKey: number, deviceId: string) {
// 通过飞奕内机列表获取实时设备数据
let detailData = await getIndoorUnitStatus(deviceId);
detailData.fanSpeed = detailData && detailData.fanSpeed ? acDeviceKeyMap[detailData.fanSpeed] : 2;
// 查询当前生效的联动配置:命中则用联动表上下限覆盖飞奕默认上下限,未命中保持飞奕默认
const linkage = await findEffectiveLinkage(deviceId, deviceType);
if (linkage) {
if (linkage.threshold_max !== null && linkage.threshold_max !== undefined && linkage.threshold_max !== '') {
detailData.maxTemp = Number(linkage.threshold_max);
}
if (linkage.threshold_min !== null && linkage.threshold_min !== undefined && linkage.threshold_min !== '') {
detailData.minTemp = Number(linkage.threshold_min);
}
}
detailData.autoControl = devices && devices.data[0].linkage_start && devices.data[0].linkage_start === "on" ? '生效' : '解除';
return {
deviceFault: {
......@@ -970,20 +982,52 @@ export async function controlAcRunning(params: {}) {
let mode = params["mode"] || "制冷";
let power = params["power"] || 'on';
let setTemp = params["setTemp"];
let maxTemp = params["maxTemp"] || 28; // 默认锁定温度上限
let minTemp = params["minTemp"] || 16; // 默认锁定温度下限
let autoControl = params["autoControl"];
let fanSpeed = params["fanSpeed"];
let maxTemp = params["maxTemp"]; // 联动温度上限
let minTemp = params["minTemp"]; // 联动温度下限
let autoControl = params["autoControl"]; // 联动是否生效
let fanSpeed = params["fanSpeed"] && params["fanSpeed"] !== "Error" ? Number(params["fanSpeed"]) : 1;
// 1. 通过设备信息,获取内机地址
let device = await selectOneDataByParam(
TABLENAME.设备表,
{ device_id: deviceId },
["id", "device_id", "region_key", "device_name", "device_ad", "control_params"]
["id", "device_id", "region_key", "device_name", "device_ad", "control_params", "device_type", "linkage_start"]
);
if (!device.data || !device.data.id) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 未注册`);
}
const deviceType = device.data.device_type || '';
// 1.1 联动开关状态写入 device.linkage_start(autoControl:生效→on,解除→off)
if (autoControl !== undefined && autoControl !== null && autoControl !== '') {
const linkageStart = autoControl === '生效' ? 'on' : 'off';
await updateManyData(TABLENAME.设备表, { device_id: deviceId }, { linkage_start: linkageStart });
}
// 1.2 联动温度上下限写入 device_linkage:与 acPop 同样方式查找,找到则更新,找不到则新增
const hasMax = maxTemp !== undefined && maxTemp !== null && maxTemp !== '';
const hasMin = minTemp !== undefined && minTemp !== null && minTemp !== '';
if (hasMax || hasMin) {
const linkage = await findEffectiveLinkage(deviceId, deviceType);
if (linkage) {
const edits: any = {};
if (hasMax) edits.threshold_max = String(maxTemp);
if (hasMin) edits.threshold_min = String(minTemp);
await updateManyData(TABLENAME.设备联动调节表, { id: linkage.id }, edits);
} else {
// 无生效配置:新增一条(有效期留空=长期生效)
const season = getSeason();
let linkageType = '';
if (deviceType.includes('新风')) linkageType = '新风';
else if (deviceType.includes('空调')) linkageType = season === 'summer' ? '制冷' : '制热';
await addData(TABLENAME.设备联动调节表, {
device_id: deviceId,
linkage_type: linkageType || '制冷',
threshold_max: hasMax ? String(maxTemp) : null,
threshold_min: hasMin ? String(minTemp) : null,
});
}
}
// 2. 接入空调控制API,更新空调设备信息
let acPrarms = {
"indoorUnitAddressFull": [deviceId],
......@@ -991,13 +1035,13 @@ export async function controlAcRunning(params: {}) {
"onOff": acDeviceKeyMap[power],
"tempSet": Number(setTemp),
"fanSpeed": acDeviceKeyMap[acFanSpeedKeyMap[fanSpeed]],
"onOffLock": acDeviceKeyMap[autoControl],
"workModeLock": acDeviceKeyMap[autoControl],
"tempSetLock": acDeviceKeyMap[autoControl],
"tempSetLo": Number(minTemp),
"tempSetHi": Number(maxTemp),
// "onOffLock": acDeviceKeyMap[autoControl],
// "workModeLock": acDeviceKeyMap[autoControl],
// "tempSetLock": acDeviceKeyMap[autoControl],
// "tempSetLo": Number(minTemp),
// "tempSetHi": Number(maxTemp),
// "openApiAction": autoControl === "生效" ? "control" : "lock"
"openApiAction": "control" // TODO:临时处理
"openApiAction": "control"
};
const result = await controlIndoorUnit(acPrarms);
// 手动控制日志
......@@ -1015,9 +1059,6 @@ export async function controlAcRunning(params: {}) {
power: power,
setTemp: setTemp,
fanSpeed: fanSpeed,
autoControl: autoControl,
maxTemp: maxTemp,
minTemp: minTemp,
success: result.success,
message: result.message || (result.success ? '控制成功' : '控制失败'),
},
......@@ -1092,6 +1133,46 @@ function getSeason(): 'summer' | 'winter' {
return (month >= 5 && month <= 9) ? 'summer' : 'winter';
}
/**
* 查找设备当前生效的联动配置(device_linkage 表)
* 规则:
* 1. 先过滤有效期:begin_validity 为空或 <= 当前时间,且 end_validity 为空或 >= 当前时间
* 2. 有效期内仅 1 条则直接返回
* 3. 多条时根据设备类型 + 当前季节匹配联动类型(空调:夏→制冷/冬→制热;新风→新风)
* 4. 无匹配返回 null
* @param deviceId 设备编号
* @param deviceType 设备类型(空调/新风等)
*/
async function findEffectiveLinkage(deviceId: string, deviceType: string): Promise<any | null> {
const res = await selectDataListByParam(
TABLENAME.设备联动调节表,
{ device_id: deviceId },
["id", "device_id", "linkage_type", "threshold_max", "threshold_min", "begin_validity", "end_validity"]
);
const rows: any[] = res.data || [];
if (rows.length === 0) return null;
const now = new Date();
// 过滤有效期内的配置
const valid = rows.filter((r: any) => {
const begin = r.begin_validity ? new Date(r.begin_validity) : null;
const end = r.end_validity ? new Date(r.end_validity) : null;
if (begin && begin > now) return false;
if (end && end < now) return false;
return true;
});
if (valid.length === 0) return null;
if (valid.length === 1) return valid[0];
// 多条:根据设备类型 + 当前季节匹配联动类型
const season = getSeason();
let targetType: string | null = null;
if (deviceType.includes('新风')) targetType = '新风';
else if (deviceType.includes('空调')) targetType = season === 'summer' ? '制冷' : '制热';
const matched = valid.find((r: any) => targetType && r.linkage_type === targetType);
return matched || null;
}
// 根据IAQ温度计算空调设定温度
/**
* 根据文档 Slide 39 三段式温度段,计算联动控制参数
......@@ -1148,22 +1229,28 @@ export async function controlAcByEnvironment(deviceId: string, data: any, region
linkage_start: 'on'
}, ["id", "device_name", "device_id", "device_ad", "device_type"]);
console.log(`若是新风设备,判断联动是否开启 ${fanDevices.data.length}`);
// 逐台按设备自身日程过滤:处于关闭/休眠时段的新风/空调不参与联动
if (fanDevices.data.length > 0) {
const fanOffFlags = await Promise.all(fanDevices.data.map((fd: any) => isInOffPeriod(fd.device_id, regionKey)));
fanDevices = { data: fanDevices.data.filter((_: any, i: number) => !fanOffFlags[i]) };
console.log(`新风联动设备按日程过滤后 ${fanDevices.data.length} 台参与控制`);
}
if (fanDevices.data.length > 0) {
// CO2联动:高→送风模式(3)+高风(1),低→不传workMode(不切换模式)+低风(4)
// const fanWorkMode: number | undefined = fanParams.action === 'high' ? 3 : undefined;
const fanWorkMode = 1;
const fanSpeedVal = fanParams.action === 'high' ? 1 : 4;
// 去重
// 去重(含参与控制的设备数,设备因日程被过滤导致数量变化时不跳过)
const fanCacheKey = `fan_${regionKey}`;
const lastFanState = envControlCache.get(fanCacheKey);
if (lastFanState && lastFanState.co2Action === fanParams.action && lastFanState.workMode === fanWorkMode && lastFanState.fanSpeed === fanSpeedVal) {
if (lastFanState && lastFanState.co2Action === fanParams.action && lastFanState.workMode === fanWorkMode && lastFanState.fanSpeed === fanSpeedVal && lastFanState.deviceCount === fanDevices.data.length) {
console.log(`新风联动跳过(参数未变): CO2 ${co2}ppm, ${fanParams.action === 'high' ? '送风高风' : '低风待机'}, 区域 ${regionKey}`);
} else {
let fanControlRes = await controlAcRunningApi(fanDevices, fanWorkMode, undefined, fanSpeedVal, 1);
// 更新缓存(含 workMode/fanSpeed 用于去重)
envControlCache.set(fanCacheKey, { co2Action: fanParams.action, workMode: fanWorkMode, fanSpeed: fanSpeedVal });
// 更新缓存(含 workMode/fanSpeed/deviceCount 用于去重)
envControlCache.set(fanCacheKey, { co2Action: fanParams.action, workMode: fanWorkMode, fanSpeed: fanSpeedVal, deviceCount: fanDevices.data.length });
let temp = calcAcTempSet(temperature, season);
......@@ -1205,6 +1292,12 @@ console.log(`若是空调设备,判断温度是否为空 ${temperature}`);
linkage_start: 'on'
}, ["id", "device_name", "device_id", "device_ad", "control_params"]);
console.log(`若是空调设备,判断联动是否开启 ${acDevices.data.length}`);
// 逐台按设备自身日程过滤:处于关闭/休眠时段的空调不参与联动
if (acDevices.data.length > 0) {
const acOffFlags = await Promise.all(acDevices.data.map((ad: any) => isInOffPeriod(ad.device_id, regionKey)));
acDevices = { data: acDevices.data.filter((_: any, i: number) => !acOffFlags[i]) };
console.log(`空调联动设备按日程过滤后 ${acDevices.data.length} 台参与控制`);
}
if (acDevices.data.length > 0) {
const { tempSet, fanSpeed, onOff } = calcAcTempSet(temperature, season);
const workMode = season === 'summer' ? 1 : 2; // 1制冷 2制热
......
......@@ -191,14 +191,14 @@ export async function addSchedule(params: {
if (deviceIds && deviceIds.length) {
// 有具体设备时,按 deviceIds 查,取出它们的 device_id + region_key
const deviceRes = await selectDataListByParam(TABLENAME.设备表, {
device_type: "空调",
device_type: { "%in%": ["空调", "新风"] },
device_id: { "%in%": deviceIds },
}, ["device_id", "region_key"]);
devices = deviceRes.data || [];
} else if (regionKeys && regionKeys.length) {
// 仅传区域时,查该区域下所有空调
const deviceRes = await selectDataListByParam(TABLENAME.设备表, {
device_type: "空调",
device_type: { "%in%": ["空调", "新风"] },
region_key: { "%in%": regionKeys },
}, ["device_id", "region_key"]);
devices = deviceRes.data || [];
......@@ -308,13 +308,13 @@ export async function editSchedule(params: {
let devices: any[] = [];
if (deviceIds && deviceIds.length) {
const deviceRes = await selectDataListByParam(TABLENAME.设备表, {
device_type: "空调",
device_type: { "%in%": ["空调", "新风"] },
device_id: { "%in%": deviceIds },
}, ["device_id", "region_key"]);
devices = deviceRes.data || [];
} else if (regionKeys && regionKeys.length) {
const deviceRes = await selectDataListByParam(TABLENAME.设备表, {
device_type: "空调",
device_type: { "%in%": ["空调", "新风"] },
region_key: { "%in%": regionKeys },
}, ["device_id", "region_key"]);
devices = deviceRes.data || [];
......
......@@ -11,7 +11,9 @@ export enum TABLENAME {
设备日志表 = 'device_logs',
日程表 = 'schedule',
日程设备关联表 = 'schedule_device',
数据接入日志表 = 'data_access_log'
数据接入日志表 = 'data_access_log',
设备联动调节表 = 'device_linkage',
设备扩展表 = 'device_expansion'
};
/**
......
......@@ -43,6 +43,11 @@ export const TablesConfig = [
}
},
association: [
],
indexes: [
{ fields: ['device_id'] },
{ fields: ['received_time'] },
{ fields: ['device_id', 'received_time'] },
]
},
// 设备故障表
......@@ -192,7 +197,13 @@ export const TablesConfig = [
},
association: [
{ type: "hasMany", target: "device_data", foreignKey: "device_id" },
{ type: "hasMany", target: "device_fault", foreignKey: "device_id" }
{ type: "hasMany", target: "device_fault", foreignKey: "device_id" },
{ type: "hasMany", target: "device_linkage", foreignKey: "device_id" },
{ type: "hasMany", target: "device_expansion", foreignKey: "device_id" }
],
indexes: [
{ fields: ['device_type'] },
{ fields: ['device_type', 'device_id'] },
]
},
// 人员信息表
......@@ -538,5 +549,148 @@ export const TablesConfig = [
{ type: "hasMany", target: "region", foreignKey: "region_key" }
]
},
// 设备联动调节表
{
tableNameCn: '设备联动调节表',
tableName: 'device_linkage',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备编号,关联device.device_id'
},
linkage_type: {
type: DataTypes.STRING(20),
allowNull: false,
comment: '联动类型:制冷/制热/新风'
},
threshold_max: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '阈值上限,如"26"'
},
threshold_min: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '阈值下限,如"22"'
},
threshold_mid: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '中间阈值,如"24"'
},
setting_max: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '上限设置:高于阈值上限时的设定值'
},
setting_min: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '下限设置:低于阈值下限时的设定值'
},
setting_mid: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '中间设置:处于阈值区间内的设定值'
},
divide_method: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '划分方式,如"按温度三段式"'
},
begin_validity: {
type: DataTypes.DATE,
allowNull: true,
comment: '有效期起,不设置则为空'
},
end_validity: {
type: DataTypes.DATE,
allowNull: true,
comment: '有效期止,不设置则为空'
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '修改时间'
}
},
association: [],
indexes: [
{ fields: ['device_id'] },
{ fields: ['linkage_type'] },
{ fields: ['device_id', 'linkage_type'] },
]
},
// 设备扩展表
{
tableNameCn: '设备扩展表',
tableName: 'device_expansion',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备ID,关联device.device_id'
},
region_key: {
type: DataTypes.BIGINT,
allowNull: false,
comment: '区域编号,关联region.id,表示设备所属区域'
},
device_type: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '设备类型,如"电能监测、电表"'
},
original_data: {
type: DataTypes.JSON,
allowNull: true,
comment: '原始扩展数据,如{usePower:114.76,electricityUsageFee:114.76}'
},
cateted_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
updated_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '修改时间'
}
},
association: [
{ type: "hasMany", target: "device", foreignKey: "device_id" },
{ type: "hasMany", target: "region", foreignKey: "region_key" }
],
indexes: [
{ fields: ['device_id'] },
{ fields: ['region_key'] },
{ fields: ['device_type'] },
{ fields: ['device_id', 'device_type'] },
]
},
];
......@@ -434,6 +434,7 @@ export async function selectPaginatedDataWithOrder( tableName:string, param:Quer
return {
data: result.rows,
total: result.count,
pagination: {
page: pageNumber,
pageSize,
......
......@@ -19,7 +19,7 @@ async function lanuch() {
/**创建http服务 */
httpServer.createServer(systemConfig.port);
/**启动MQTT订阅服务 */
await mqttSrv.startMqttClient();
// await mqttSrv.startMqttClient();
/**启动定时任务 */
startSchedule();
/**启动预警工单定时任务(10分钟) */
......
......@@ -23,3 +23,11 @@ function success(data) {
this.res.send(resultPack);
}
/**
* 中间件错误返回方法
* @param msg
*/
function error(msg) {
this.res.send({success:false, msg, code:400});
}
......@@ -8,9 +8,36 @@ import * as regionBiz from "../biz/region";
import * as scheduleBiz from "../biz/schedule";
import * as deviceBiz from "../biz/device";
import * as alertWorkOrderBiz from "../biz/deviceFault";
import * as deviceLinkageBiz from "../biz/deviceLinkage";
import { reloadScheduleTasks } from "../biz/scheduleExecutor";
import { eccReqParamater } from "../util/verificationParam";
/** 校验分页参数,不通过则直接返回错误 */
function validatePagination(pageNumber: number, pageSize: number, res: any): boolean {
if (!Number.isInteger(pageNumber) || pageNumber < 1) {
res.success({ success: false, msg: "pageNumber 必须为大于等于 1 的整数", code: 400 });
return false;
}
if (!Number.isInteger(pageSize) || pageSize < 1) {
res.success({ success: false, msg: "pageSize 必须为大于等于 1 的整数", code: 400 });
return false;
}
return true;
}
/** 校验日期格式 yyyy-MM-dd,不通过则直接返回错误 */
function validateDateParam(dateStr: string | undefined, res: any): boolean {
if (!dateStr || dateStr.trim() === '') {
res.success({ success: false, msg: "date 参数不能为空", code: 400 });
return false;
}
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr.trim())) {
res.success({ success: false, msg: "date 格式错误,应为 yyyy-MM-dd", code: 400 });
return false;
}
return true;
}
export function setRouter(httpServer) {
/** 获取周报告JSON数据 */
httpServer.get("/api/qdm/report/weekly/data", asyncHandler(getReportData));
......@@ -51,6 +78,19 @@ export function setRouter(httpServer) {
httpServer.get("/api/qdm/admin/fault/list", asyncHandler(getDeviceFaultList));
/** 日志管理 - 分页列表 */
httpServer.get("/api/qdm/admin/device/logs", asyncHandler(getDeviceLogList));
/** 设备联动参数 - 分页列表 */
httpServer.get("/api/qdm/admin/linkage/list", asyncHandler(getLinkageList));
/** 设备联动参数 - 详情 */
httpServer.get("/api/qdm/admin/linkage/detail", asyncHandler(getLinkageDetail));
/** 设备联动参数 - 新增 */
httpServer.post("/api/qdm/admin/linkage/add", asyncHandler(addLinkage));
/** 设备联动参数 - 批量新增 */
httpServer.post("/api/qdm/admin/linkage/add_batch", asyncHandler(batchAddLinkage));
/** 设备联动参数 - 编辑 */
httpServer.post("/api/qdm/admin/linkage/edit", asyncHandler(editLinkage));
/** 设备联动参数 - 删除 */
httpServer.post("/api/qdm/admin/linkage/delete", asyncHandler(deleteLinkage));
}
/**
......@@ -100,9 +140,13 @@ async function getRegionDevices(req, res) {
* query: pageNumber, pageSize, startDate, endDate, regionKey
*/
async function getScheduleList(req, res) {
const pageNumber = Number(req.query?.pageNumber) || 1;
const pageSize = Number(req.query?.pageSize) || 10;
if (!validatePagination(pageNumber, pageSize, res)) return;
const data = await scheduleBiz.getScheduleList({
pageNumber: Number(req.query?.pageNumber) || 1,
pageSize: Number(req.query?.pageSize) || 10,
pageNumber,
pageSize,
startDate: req.query && req.query.startDate?req.query.startDate:'',
endDate: req.query && req.query.endDate?req.query.endDate:'',
deviceIds: req.query?.deviceIds ? req.query.deviceIds : undefined,
......@@ -236,14 +280,14 @@ async function getDeviceList(req, res) {
* body: { deviceId, power, setTemp, fanSpeed, deviceMode }
*/
async function editDevice(req, res) {
let reqConf = { deviceId: 'String', power: 'String', setTemp: 'Number', fanSpeed: 'String', deviceMode: 'String', lock: 'String' };
const NotMustHaveKeys = ["fanSpeed", "deviceMode", "lock"];
let { deviceId, power, setTemp, fanSpeed, deviceMode, lock } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
let reqConf = { deviceId: 'String', power: 'String', setTemp: 'Number', fanSpeed: 'String', deviceMode: 'String', locked: 'String' };
const NotMustHaveKeys = ["fanSpeed", "deviceMode", "locked"];
let { deviceId, power, setTemp, fanSpeed, deviceMode, locked } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!deviceId || !power) {
res.error("缺少必填参数:deviceId, power");
return;
}
const data = await deviceBiz.editAcDeviceControl({ deviceId, power, setTemp, fanSpeed, deviceMode, lock });
const data = await deviceBiz.editAcDeviceControl({ deviceId, power, setTemp, fanSpeed, deviceMode, locked });
res.success(data);
}
......@@ -379,9 +423,13 @@ async function batchEditLocalDevices(req, res) {
* query: pageNumber, pageSize, level, fault_code, device_id, startTime, endTime
*/
async function getDeviceFaultList(req, res) {
const pageNumber = Number(req.query?.pageNumber) || 1;
const pageSize = Number(req.query?.pageSize) || 10;
if (!validatePagination(pageNumber, pageSize, res)) return;
const data = await alertWorkOrderBiz.getDeviceFaultList({
pageNumber: Number(req.query?.pageNumber) || 1,
pageSize: Number(req.query?.pageSize) || 10,
pageNumber,
pageSize,
level: req.query?.level ? Number(req.query.level) : undefined,
fault_code: req.query?.fault_code || undefined,
device_id: req.query?.device_id || undefined,
......@@ -399,9 +447,14 @@ async function getDeviceFaultList(req, res) {
* query: pageNumber, pageSize, deviceName(设备名称模糊匹配), status(0失败 1成功)
*/
async function getDeviceLogList(req, res) {
const pageNumber = Number(req.query?.pageNumber) || 1;
const pageSize = Number(req.query?.pageSize) || 10;
if (!validatePagination(pageNumber, pageSize, res)) return;
const data = await deviceBiz.getDeviceLogList({
pageNumber: Number(req.query?.pageNumber) || 1,
pageSize: Number(req.query?.pageSize) || 10,
pageNumber,
pageSize,
deviceId: req.query?.deviceId || undefined,
deviceName: req.query?.deviceName || undefined,
status: req.query?.status !== undefined && req.query.status !== '' ? Number(req.query.status) : undefined,
});
......@@ -415,7 +468,12 @@ async function getDeviceLogList(req, res) {
* 每小时最高温度和人流展示与对比(今天 vs 昨天)
*/
async function getHourlyAnalysis(req, res) {
const data = await reportBiz.getHourlyTempVisitorComparison(req.query?.date);
const date = req.query?.date;
if (date !== undefined) {
// 传了 date 就校验格式
if (!validateDateParam(date, res)) return;
}
const data = await reportBiz.getHourlyTempVisitorComparison(date);
res.success(data);
}
......@@ -424,7 +482,201 @@ async function getHourlyAnalysis(req, res) {
* 每天最高温度和人流展示与对比(近 N 天趋势)
*/
async function getDailyAnalysis(req, res) {
const days = Number(req.query?.days) || 7;
const rawDays = req.query?.days;
let days: number;
if (rawDays === undefined || rawDays === '' || rawDays === null) {
days = 7;
} else {
days = Number(rawDays);
if (!Number.isInteger(days) || days < 1) {
res.success({ success: false, msg: "days 必须为大于等于 1 的整数", code: 400 });
return;
}
}
if (days > 365) {
days = 365;
}
const data = await reportBiz.getDailyTempVisitorComparison(days);
res.success(data);
}
// ======================= 设备联动参数管理 =======================
/**
* GET /api/qdm/admin/linkage/list
* 设备联动参数分页列表
* query: pageNumber, pageSize, deviceName(设备名称模糊匹配), linkageType(制冷/制热/新风)
*/
async function getLinkageList(req, res) {
const data = await deviceLinkageBiz.getLinkageList({
pageNumber: Number(req.query?.pageNumber) || 1,
pageSize: Number(req.query?.pageSize) || 10,
deviceId: req.query?.deviceId || undefined,
deviceName: req.query?.deviceName || undefined,
linkageType: req.query?.linkageType || undefined,
});
res.success(data);
}
/**
* GET /api/qdm/admin/linkage/detail?id=1
* 设备联动参数详情
* query: id
*/
async function getLinkageDetail(req, res) {
const id = Number(req.query?.id);
if (!id) {
res.error("缺少必填参数:id");
return;
}
const data = await deviceLinkageBiz.getLinkageDetail(id);
if (data.isSuccess) {
res.success(data.data);
} else {
res.error(data.msg);
}
}
/**
* POST /api/qdm/admin/linkage/add
* 新增设备联动参数
* body: deviceId, linkageType, thresholdMax, thresholdMin, thresholdMid,
* settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity
*/
async function addLinkage(req, res) {
let reqConf = {
deviceId: 'String',
linkageType: 'String',
thresholdMax: 'String',
thresholdMin: 'String',
thresholdMid: 'String',
settingMax: 'String',
settingMin: 'String',
settingMid: 'String',
divideMethod: 'String',
beginValidity: 'Date',
endValidity: 'Date',
};
const NotMustHaveKeys = ["thresholdMax", "thresholdMin", "thresholdMid", "settingMax", "settingMin", "settingMid", "divideMethod", "beginValidity", "endValidity"];
let { deviceId, linkageType, thresholdMax, thresholdMin, thresholdMid, settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity } =
eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!deviceId || !linkageType) {
res.error("缺少必填参数:deviceId, linkageType");
return;
}
const data = await deviceLinkageBiz.addLinkage({
deviceId,
linkageType,
thresholdMax: thresholdMax ?? undefined,
thresholdMin: thresholdMin ?? undefined,
thresholdMid: thresholdMid ?? undefined,
settingMax: settingMax ?? undefined,
settingMin: settingMin ?? undefined,
settingMid: settingMid ?? undefined,
divideMethod: divideMethod ?? undefined,
beginValidity: beginValidity ?? undefined,
endValidity: endValidity ?? undefined,
});
res.success(data);
}
/**
* POST /api/qdm/admin/linkage/add_batch
* 批量新增设备联动参数(为多台设备配置相同规则)
* body: deviceIds(数组), linkageType, thresholdMax, thresholdMin, thresholdMid,
* settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity
*/
async function batchAddLinkage(req, res) {
let reqConf = {
deviceIds: 'Array',
linkageType: 'String',
thresholdMax: 'String',
thresholdMin: 'String',
thresholdMid: 'String',
settingMax: 'String',
settingMin: 'String',
settingMid: 'String',
divideMethod: 'String',
beginValidity: 'Date',
endValidity: 'Date',
};
const NotMustHaveKeys = ["thresholdMax", "thresholdMin", "thresholdMid", "settingMax", "settingMin", "settingMid", "divideMethod", "beginValidity", "endValidity"];
let { deviceIds, linkageType, thresholdMax, thresholdMin, thresholdMid, settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity } =
eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0 || !linkageType) {
res.error("缺少必填参数:deviceIds, linkageType");
return;
}
const data = await deviceLinkageBiz.batchAddLinkage({
deviceIds,
linkageType,
thresholdMax: thresholdMax ?? undefined,
thresholdMin: thresholdMin ?? undefined,
thresholdMid: thresholdMid ?? undefined,
settingMax: settingMax ?? undefined,
settingMin: settingMin ?? undefined,
settingMid: settingMid ?? undefined,
divideMethod: divideMethod ?? undefined,
beginValidity: beginValidity ?? undefined,
endValidity: endValidity ?? undefined,
});
res.success(data);
}
/**
* POST /api/qdm/admin/linkage/edit
* 编辑设备联动参数
* body: id + 待更新字段(同新增)
*/
async function editLinkage(req, res) {
let reqConf = {
id: 'String',
deviceId: 'String',
linkageType: 'String',
thresholdMax: 'String',
thresholdMin: 'String',
thresholdMid: 'String',
settingMax: 'String',
settingMin: 'String',
settingMid: 'String',
divideMethod: 'String',
beginValidity: 'Date',
endValidity: 'Date',
};
const NotMustHaveKeys = ["deviceId", "linkageType", "thresholdMax", "thresholdMin", "thresholdMid", "settingMax", "settingMin", "settingMid", "divideMethod", "beginValidity", "endValidity"];
let { id, deviceId, linkageType, thresholdMax, thresholdMin, thresholdMid, settingMax, settingMin, settingMid, divideMethod, beginValidity, endValidity } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!id) {
res.error("缺少必填参数:id");
return;
}
const data = await deviceLinkageBiz.editLinkage({
id: Number(id),
deviceId: deviceId ?? undefined,
linkageType: linkageType ?? undefined,
thresholdMax: thresholdMax ?? undefined,
thresholdMin: thresholdMin ?? undefined,
thresholdMid: thresholdMid ?? undefined,
settingMax: settingMax ?? undefined,
settingMin: settingMin ?? undefined,
settingMid: settingMid ?? undefined,
divideMethod: divideMethod ?? undefined,
beginValidity: beginValidity ?? undefined,
endValidity: endValidity ?? undefined,
});
res.success(data);
}
/**
* POST /api/qdm/admin/linkage/delete
* 删除设备联动参数
* body: id
*/
async function deleteLinkage(req, res) {
const { id } = req.body || {};
if (!id) {
res.error("缺少必填参数:id");
return;
}
const data = await deviceLinkageBiz.deleteLinkage(Number(id));
res.success(data);
}
......@@ -34,6 +34,12 @@ export function setRouter(httpServer) {
httpServer.post('/api/qdm/run/monitor/acRun', asyncHandler(controlAcRunning));
/** 运行分析 - 环境质量弹窗 */
httpServer.post('/api/qdm/run/analysis/envPop', asyncHandler(getRunAnalysisEnvPop));
/** 运行分析 - 离线设备弹窗 */
httpServer.post('/api/qdm/run/analysis/offlineDevicePop', asyncHandler(getOfflineDevicePop));
/** 运行分析 - 故障设备弹窗 */
httpServer.post('/api/qdm/run/analysis/faultDevicePop', asyncHandler(getFaultDevicePop));
/** 运行分析 - 设备能耗监控弹窗 */
httpServer.post('/api/qdm/run/analysis/deviceEnergyPop', asyncHandler(getDeviceEnergyPop));
/** 启动数据集成 */
httpServer.post('/api/qdm/run/start/schedule', asyncHandler(startSchedule));
......@@ -201,6 +207,39 @@ async function getRunAnalysisEnvPop(req, res) {
}
/**
* 运行分析 - 离线设备弹窗
*/
async function getOfflineDevicePop(req, res) {
let reqConf = { regionGroup: 'String', regionType: 'String' };
const NotMustHaveKeys = ["regionGroup", "regionType"];
let { regionGroup, regionType } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await deviceBiz.getOfflineDeviceList(regionGroup, regionType);
res.success(result);
}
/**
* 运行分析 - 故障设备弹窗
*/
async function getFaultDevicePop(req, res) {
let reqConf = { regionGroup: 'String', regionType: 'String' };
const NotMustHaveKeys = ["regionGroup", "regionType"];
let { regionGroup, regionType } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await deviceBiz.getFaultDeviceList(regionGroup, regionType);
res.success(result);
}
/**
* 运行分析 - 设备能耗监控弹窗(各区域昨日/今日能耗)
*/
async function getDeviceEnergyPop(req, res) {
let reqConf = { regionGroup: 'String', regionType: 'String' };
const NotMustHaveKeys = ["regionGroup", "regionType"];
let { regionGroup, regionType } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await deviceBiz.getDeviceEnergyList(regionGroup, regionType);
res.success(result);
}
/**
* 本月累计用电(按天)
*/
async function getMonthlyEnergy(req, res) {
......
......@@ -14,8 +14,22 @@ export function setRouter(httpServer) {
httpServer.post('/api/qdm/feiyi/indoorUnit/list', asyncHandler(getFeiyiIndoorUnitList));
/** 空调内机控制 */
httpServer.post('/api/qdm/feiyi/indoorUnit/control', asyncHandler(getFeiyiIndoorUnitControl));
/** 单个内机分摊电量和电费 */
httpServer.post('/api/qdm/feiyi/indoorUnit/expenses', asyncHandler(getIndoorUnitExpensesApi));
/** 房间电费日使用详情 */
httpServer.post('/api/qdm/feiyi/indoorUnit/expenses/list', asyncHandler(getRoomExpensesListApi));
/** 电费数据写入设备扩展表 */
httpServer.post('/api/qdm/feiyi/indoorUnit/expenses/save', asyncHandler(saveRoomExpensesToExpansionApi));
/** 机房群控设备最新参数数据(测试接口) */
httpServer.get('/api/qdm/feiyi/roomGroup/data', asyncHandler(getRoomGroupDeviceDataApi));
/** 机房群控获取网关信息(测试接口) */
httpServer.get('/api/qdm/feiyi/roomGroup/gatewayInfo', asyncHandler(getRoomGroupGatewayInfoApi));
/** 机房群控参数写入(测试接口) */
httpServer.post('/api/qdm/feiyi/roomGroup/writeParams', asyncHandler(setRoomGroupParamsApi));
}
// =============================================== 控制系统 ==================================================
/**
* 网关设备列表
......@@ -81,4 +95,104 @@ async function getFeiyiIndoorUnitControl(req, res) {
res.success(result);
}
/**
* 单个内机分摊电量和电费
* @param page 当前页码(选填)
* @param limit 每页条数(选填)
* @param roomId 房间ID(选填)
* @param indoorUnitId 内机ID(选填)
* @param indoorUnitAddressFull 内机地址(选填)
* @param occurrenceTime 日期 yyyyMMdd(必填)
*/
async function getIndoorUnitExpensesApi(req, res) {
let reqConf = {
page: 'Number', limit: 'Number',
roomId: 'String', indoorUnitId: 'String', indoorUnitAddressFull: 'String',
occurrenceTime: 'String',
};
const NotMustHaveKeys = [ "page", "limit", "roomId", "indoorUnitId", "indoorUnitAddressFull" ];
let params = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await feiyiClientBiz.getIndoorUnitExpenses(params);
res.success(result);
}
/**
* 房间电费日使用详情
* @param page 当前页码(选填)
* @param limit 每页条数(选填)
* @param roomIds 房间ID集合(选填)
* @param floorIds 楼层ID集合(选填)
* @param buildingIds 楼栋ID集合(选填)
* @param occurrenceTime 日期 yyyyMMdd(必填)
*/
async function getRoomExpensesListApi(req, res) {
let reqConf = {
page: 'Number', limit: 'Number',
roomIds: 'Array', floorIds: 'Array', buildingIds: 'Array',
occurrenceTime: 'String',
};
const NotMustHaveKeys = [ "page", "limit", "roomIds", "floorIds", "buildingIds" ];
let params = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await feiyiClientBiz.getRoomExpensesList(params);
res.success(result);
}
/**
* 电费数据写入设备扩展表 device_expansion
* 拉取"房间电费日使用详情",按房间(region)映射到本地设备,写入 device_expansion
* @param occurrenceTime 日期 yyyyMMdd(必填)
* @param deviceType 写入的设备类型,默认"电能监测"
*/
async function saveRoomExpensesToExpansionApi(req, res) {
let reqConf = { occurrenceTime: 'String', deviceType: 'String' };
const NotMustHaveKeys = [ "deviceType" ];
let params = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await feiyiClientBiz.saveRoomExpensesToExpansion(
params.occurrenceTime,
params.deviceType || '电能监测'
);
res.success(result);
}
// =============================================== 机房系统 ==================================================
/**
* 机房群控设备最新参数数据
* @param gwSn 网关编号(可选)
* @param deviceAddr 设备地址
*/
async function getRoomGroupDeviceDataApi(req, res) {
let reqConf = { gwSn: 'String', deviceAddr: 'String' };
const NotMustHaveKeys = [ "gwSn" ];
let params = eccReqParamater(reqConf, req.query, NotMustHaveKeys);
const result = await feiyiClientBiz.getRoomGroupDeviceData(params.gwSn, params.deviceAddr);
res.success(result);
}
/**
* 机房群控获取网关信息
* @param projectId 项目ID
*/
async function getRoomGroupGatewayInfoApi(req, res) {
let reqConf = { projectId: 'String' };
const NotMustHaveKeys: string[] = [];
let params = eccReqParamater(reqConf, req.query, NotMustHaveKeys);
const result = await feiyiClientBiz.getRoomGroupGatewayInfo(params.projectId);
res.success(result);
}
/**
* 机房群控参数写入
* @param addr 设备地址
* @param gwId 网关ID(可选)
* @param projectId 项目ID
* @param params 写入参数 JSON,如 { "1": 0.0 }
*/
async function setRoomGroupParamsApi(req, res) {
let reqConf = { addr: 'String', gwId: 'Number', projectId: 'String', params: 'Object' };
const NotMustHaveKeys = [ "gwId" ];
let params = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
const result = await feiyiClientBiz.setRoomGroupParams(params.addr, params.gwId, params.projectId, params.params);
res.success(result);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment