设备联动接口

parent 42987c2f
import { selectPaginatedDataWithOrder, selectDataListByParam } from "../data/findData";
import { TABLENAME } from "../config/dbEnum";
/** 故障等级映射 */
const levelMap: Record<number, string> = {
1: '一级',
2: '二级',
3: '三级',
};
/** 处理状态映射 */
const statusMap: Record<number, string> = {
0: '未处理',
1: '处理中',
2: '已解决',
};
export interface AlertWorkOrderQueryParams {
pageNumber?: number;
pageSize?: number;
level?: number; // 故障等级:1/2/3
fault_code?: string; // 故障代码(模糊匹配)
device_id?: string; // 设备ID(模糊匹配)
startTime?: string; // 时间范围起始 'YYYY-MM-DD HH:mm:ss'
endTime?: string; // 时间范围截止 'YYYY-MM-DD HH:mm:ss'
}
/**
* 预警工单列表查询(分页)
*/
export async function getDeviceFaultList(params: AlertWorkOrderQueryParams) {
const pageNumber = params.pageNumber || 1;
const pageSize = params.pageSize || 10;
// 构建查询条件
const queryParam: any = {};
if (params.level !== undefined && params.level !== null) {
queryParam.level = params.level;
}
if (params.fault_code) {
queryParam.fault_code = { "%like%": params.fault_code };
}
if (params.device_id) {
queryParam.device_id = { "%like%": params.device_id };
}
if (params.startTime || params.endTime) {
const start = params.startTime || '1970-01-01 00:00:00';
const end = params.endTime || '2099-12-31 23:59:59';
queryParam.occurred_time = { "%between%": [start, end] };
}
// 分页查询,按创建时间倒序
const result = await selectPaginatedDataWithOrder(
TABLENAME.设备故障表,
{ ...queryParam },
["id", "device_id", "fault_type", "fault_code", "fault_description", "occurred_time", "level", "status", "created_at"],
pageNumber,
pageSize,
[["created_at", "DESC"]],
);
// 通过设备编号查询所属区域,拼接 regionName(groups_type_name)
const rows = (result.data as any[]) || [];
const deviceIds = [...new Set(rows.map((item: any) => item.device_id).filter(Boolean))];
const deviceRegionNameMap: { [deviceId: string]: string } = {};
if (deviceIds.length > 0) {
const deviceResult = await selectDataListByParam(
TABLENAME.设备表,
{ device_id: { "%in%": deviceIds } },
["device_id", "region_key"]
);
const regionKeys = [...new Set((deviceResult.data || []).map((d: any) => d.region_key).filter(Boolean))];
const regionMap: { [id: number]: any } = {};
if (regionKeys.length > 0) {
const regionResult = await selectDataListByParam(
TABLENAME.区域表,
{ id: { "%in%": regionKeys } },
["id", "groups", "type", "name"]
);
(regionResult.data || []).forEach((r: any) => {
regionMap[r.id] = r;
});
}
(deviceResult.data || []).forEach((d: any) => {
const region = regionMap[d.region_key];
deviceRegionNameMap[d.device_id] = region
? [region.groups, region.type, region.name].filter(Boolean).join('_')
: '';
});
}
// 映射中文名
const list = rows.map((item: any) => ({
id: item.id,
device_id: item.device_id || '',
regionName: deviceRegionNameMap[item.device_id] || '',
fault_type: item.fault_type || '',
fault_code: item.fault_code || '',
fault_description: item.fault_description || '',
occurred_time: item.occurred_time || '',
created_at: item.created_at || '',
level: item.level,
levelName: levelMap[item.level] || '未知',
status: item.status,
statusName: statusMap[item.status] || '未知',
order_address: '', // 预留:工单地址,todo
order_send: 'on', // 预留:工单推送,默认 on,todo
}));
return {
list,
total: result.pagination.total,
pagination: result.pagination,
};
}
...@@ -27,6 +27,9 @@ import { ...@@ -27,6 +27,9 @@ import {
formatMonthKey, formatMonthKey,
} from '../util/dateUtils'; } from '../util/dateUtils';
import { get } from 'http'; import { get } from 'http';
import { formatLocalTime, getSeason } from '../tools/systemTools';
import { addData } from '../data/addData';
import { updateManyData } from '../data/updateData';
// ==================== 质量判定函数(从 device.ts 移植,适配新系统) ==================== // ==================== 质量判定函数(从 device.ts 移植,适配新系统) ====================
...@@ -2315,7 +2318,7 @@ export async function getAcRunningPop(regionKey: string, deviceId: string) { ...@@ -2315,7 +2318,7 @@ export async function getAcRunningPop(regionKey: string, deviceId: string) {
let devices = await selectDataListByParam( let devices = await selectDataListByParam(
TABLENAME.设备表, TABLENAME.设备表,
{ device_id: deviceId }, // region_key: regionKey, { device_id: deviceId }, // region_key: regionKey,
["device_id", "device_type", "device_name", "device_param"] ["device_id", "device_type", "device_name", "linkage_start", "device_param"]
); );
if (!devices.data.length) { if (!devices.data.length) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 在区域 ${regionKey} 中未找到`); throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 在区域 ${regionKey} 中未找到`);
...@@ -2334,6 +2337,7 @@ export async function getAcRunningPop(regionKey: string, deviceId: string) { ...@@ -2334,6 +2337,7 @@ export async function getAcRunningPop(regionKey: string, deviceId: string) {
return { return {
deviceInfo: devices.data[0].device_param, deviceInfo: devices.data[0].device_param,
autoControl: devices.data[0].linkage_start,
latestData: deviceData latestData: deviceData
} }
} }
...@@ -2347,9 +2351,9 @@ export async function controlAcRunning(params: {}) { ...@@ -2347,9 +2351,9 @@ export async function controlAcRunning(params: {}) {
let mode = params["mode"]; let mode = params["mode"];
let power = params["power"] || "on"; let power = params["power"] || "on";
let setTemp = params["setTemp"]; let setTemp = params["setTemp"];
let maxTemp = params["maxTemp"] || 30; // 默认锁定温度上限 let maxTemp = params["maxTemp"] || 30; // 联动温度上限
let minTemp = params["minTemp"] || 16; // 默认锁定温度下限 let minTemp = params["minTemp"] || 16; // 联动温度下限
let autoControl = params["autoControl"] || "生效"; let autoControl = params["autoControl"] || "生效"; // 联动是否生效
let fanSpeed = acPopFanSpeedMap[params["fanSpeed"]]; let fanSpeed = acPopFanSpeedMap[params["fanSpeed"]];
console.log(fanSpeed) console.log(fanSpeed)
...@@ -2357,11 +2361,46 @@ export async function controlAcRunning(params: {}) { ...@@ -2357,11 +2361,46 @@ export async function controlAcRunning(params: {}) {
let device = await selectOneDataByParam( let device = await selectOneDataByParam(
TABLENAME.设备表, TABLENAME.设备表,
{ device_id: deviceId }, { device_id: deviceId },
["id", "device_id", "region_key", "device_name", "device_param"] ["id", "device_id", "region_key", "device_name", "device_param", "linkage_start"]
); );
if (!device.data || !device.data.id) { if (!device.data || !device.data.id) {
throw new BizError(ERRORENUM.未找到数据, `设备 ${deviceId} 未注册`); 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,更新空调设备信息 // 2. 接入空调控制API,更新空调设备信息
let acPrarms = { let acPrarms = {
"indoorUnitAddressFull": [deviceId], "indoorUnitAddressFull": [deviceId],
...@@ -2369,14 +2408,84 @@ export async function controlAcRunning(params: {}) { ...@@ -2369,14 +2408,84 @@ export async function controlAcRunning(params: {}) {
"onOff": powerMap[power], "onOff": powerMap[power],
"tempSet": Number(setTemp), "tempSet": Number(setTemp),
"fanSpeed": fanSpeedMap[fanSpeed], "fanSpeed": fanSpeedMap[fanSpeed],
"onOffLock": lockMap[autoControl], // "onOffLock": lockMap[autoControl],
"workModeLock": lockMap[autoControl], // "workModeLock": lockMap[autoControl],
"tempSetLock": lockMap[autoControl], // "tempSetLock": lockMap[autoControl],
"tempSetLo": Number(minTemp), // "tempSetLo": Number(minTemp),
"tempSetHi": Number(maxTemp), // "tempSetHi": Number(maxTemp),
"openApiAction": autoControl === "生效" ? "control" : "lock" "openApiAction": "control"
}; };
const result = await controlIndoorUnit(acPrarms);
// 手动控制日志
const now = new Date();
const operationTime = new Date();
const operationTimeStr = formatLocalTime(operationTime);
const acLogs = [{
device_id: deviceId,
device_type: '空调',
operation_content: {
option: '手动控制',
device_id: deviceId,
time: operationTimeStr,
mode: mode,
power: power,
setTemp: setTemp,
fanSpeed: fanSpeed,
success: result.success,
message: result.message || (result.success ? '控制成功' : '控制失败'),
},
status: result.success ? 1 : 0,
created_at: now,
updated_at: now,
}];
await addData(TABLENAME.设备日志表, acLogs);
return await controlIndoorUnit(acPrarms); return await controlIndoorUnit(acPrarms);
} }
/**
* 查找设备当前生效的联动配置(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", "threshold_mid",
"setting_max", "setting_min", "setting_mid",
"fan_speed_max", "fan_speed_mid", "fan_speed_min", "is_use",
"begin_validity", "end_validity"]
);
const rows: any[] = res.data || [];
if (rows.length === 0) return null;
const now = new Date();
// 过滤有效期 + 启用(is_use=on)的配置
const valid = rows.filter((r: any) => {
if (r.is_use === 'off') return false;
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;
}
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
* 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss' * 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss'
* 替代 toISOString() 避免 UTC 时区偏移问题 * 替代 toISOString() 避免 UTC 时区偏移问题
*/ */
export function formatLocalTime(date: Date): string { // export function formatLocalTime(date: Date): string {
const pad = (n: number) => String(n).padStart(2, '0'); // const pad = (n: number) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; // return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
} // }
...@@ -197,6 +197,12 @@ export const TablesConfig = [ ...@@ -197,6 +197,12 @@ export const TablesConfig = [
defaultValue: 0, defaultValue: 0,
comment: '设备状态:0=在线, 1=离线, 2=故障' comment: '设备状态:0=在线, 1=离线, 2=故障'
}, },
linkage_start: {
type: DataTypes.STRING(10),
allowNull: true,
defaultValue: 'off',
comment: '联动启用:on=开 off=关'
},
device_name: { device_name: {
type: Sequelize.STRING(255), type: Sequelize.STRING(255),
allowNull: false, allowNull: false,
......
...@@ -113,10 +113,10 @@ async function controlDeviceRunning(req, res) { ...@@ -113,10 +113,10 @@ async function controlDeviceRunning(req, res) {
* 控制空调运行 * 控制空调运行
*/ */
async function controlAcRunning(req, res) { async function controlAcRunning(req, res) {
let reqConf = {deviceId:'String', mode:'String', power:'String', setTemp:'String', maxTemp:'String', minTemp:'String', autoControl:'String', fanSpeed:'String', regionKey:"String"}; let reqConf = {deviceId:'String', mode:'String', power:'String', setTemp:'String', maxTemp:'String', minTemp:'String', autoControl:'String', fanSpeed:'String'};
const NotMustHaveKeys = [ 'power', 'maxTemp', 'minTemp', 'autoControl', 'regionKey' ]; const NotMustHaveKeys = [ 'mode', 'power', 'setTemp', 'maxTemp', 'minTemp', 'autoControl', 'fanSpeed' ];
let { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed, regionKey } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
let params = { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed, regionKey }; let params = { deviceId, mode, power, setTemp, maxTemp, minTemp, autoControl, fanSpeed };
// const result = await runningBiz.controlAcRunning(params); const result = await runningBiz.controlAcRunning(params);
res.success({success:true}); res.success({success:true});
} }
\ No newline at end of file
...@@ -73,3 +73,18 @@ export function getFileType(fileName) { ...@@ -73,3 +73,18 @@ export function getFileType(fileName) {
return fileType; return fileType;
} }
/**
* 将 Date 对象格式化为本地时间字符串 'YYYY-MM-DD HH:mm:ss'
* 替代 toISOString() 避免 UTC 时区偏移问题
*/
export function formatLocalTime(date: Date): string {
const pad = (n: number) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
// 判断当前季节:5-9月为夏季(制冷),其余为冬季(制热)
export function getSeason(): 'summer' | 'winter' {
const month = new Date().getMonth() + 1;
return (month >= 5 && month <= 9) ? 'summer' : 'winter';
}
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