空调联动管理

parent a716b29d
/**
* 环境联动控制:迪勤环境数据同步后,根据 device_linkage 联动设置表控制空调设备
*
* 触发时机:dataSyncTask -> diqinSyncAll() 拉取环境数据后调用 acLinkageControl()
*
* 联动规则(阈值区间判定,阈值/设定值均来自 device_linkage 配置):
* - 制冷: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 -> fan_speed_max;中间区间 -> fan_speed_mid;< threshold_min -> fan_speed_min
*
* 下发策略(避免接口压力 + 人为修改检测):
* 1. 目标状态(开关机/设定温度/风速/模式)与上次联动记录不一致 -> 跨越阈值,下发
* 2. 目标状态一致 -> 查询飞奕实际状态,若与实际不符(被人为修改)-> 下发纠正
* 3. 实际状态与目标一致 -> 跳过,不下发
*
* 控制动作:任何阈值区间均保持开机,每次下发均携带 workMode,成功/失败均记录详细日志。
*/
import { selectDataListByParam, selectOneDataByParam } from '../data/findData';
import { addData } from '../data/addData';
import { updateManyData } from '../data/updateData';
import { TABLENAME } from '../config/dbEnum';
import { powerMap, workModeMap, fanSpeedMap, WORK_MODE_TEXT_MAP, FAN_SPEED_TEXT_MAP } from '../config/businessEnum';
import { controlIndoorUnit, getIndoorUnitStatus } from './feiyiClient';
import { formatLocalTime } from '../tools/systemTools';
import { getHoursAgo } from '../util/dateUtils';
/** 联动类型 -> 飞奕 workMode 数值(新风机组走送风模式) */
const WORK_MODE_FOR_LINKAGE: Record<string, number> = {
'制冷': workModeMap['制冷'],
'制热': workModeMap['制热'],
'新风': workModeMap['送风'],
};
/**
* 空调自动联动控制入口
* 无参与联动的设备 / 无有效联动配置 / 无环境数据时自动跳过,不控制空调
*/
export async function acLinkageControl(): Promise<void> {
try {
// 1. 查询启用了联动且有区域归属的空调设备
const devRes = await selectDataListByParam(
TABLENAME.设备表,
{ device_type: '空调', linkage_start: 'on' },
['device_id', 'device_name', 'region_key']
);
const acDevices = (devRes.data || []).filter((d: any) => d.region_key);
if (acDevices.length === 0) return;
// 2. 查询这些设备的有效联动配置(is_use=on 且处于有效期)
const deviceIds = acDevices.map((d: any) => d.device_id);
const linkageRes = await selectDataListByParam(
TABLENAME.设备联动调节表,
{ device_id: { '%in%': deviceIds }, is_use: 'on' }
);
const now = new Date();
const linkageMap = new Map<string, any>();
for (const row of (linkageRes.data || [])) {
const r = row.get ? row.get({ plain: true }) : row;
if (!r.device_id || r.is_use === 'off') continue;
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) continue;
if (end && end < now) continue;
if (!linkageMap.has(r.device_id)) linkageMap.set(r.device_id, r);
}
if (linkageMap.size === 0) return;
// 3. 查询 IEQ 环境传感器设备及其区域归属
const ieqRes = await selectDataListByParam(
TABLENAME.设备表,
{ device_type: 'IEQ传感器' },
['device_id', 'region_key']
);
const deviceRegionMap = new Map<string, string>();
for (const row of (ieqRes.data || [])) {
const d = row.get ? row.get({ plain: true }) : row;
if (d.region_key) deviceRegionMap.set(d.device_id, d.region_key);
}
if (deviceRegionMap.size === 0) return;
// 4. 查询这些 IEQ 最近 24h 的数据(按时间降序),每个区域只取最新一条
const ieqIds = [...deviceRegionMap.keys()];
const dataRes = await selectDataListByParam(
TABLENAME.设备数据表,
{
device_id: { '%in%': ieqIds },
device_time: { '%gte%': getHoursAgo(24) },
'%orderDesc%': 'device_time',
},
['device_id', 'device_data', 'device_time']
);
const regionEnvMap = new Map<string, any>();
for (const row of (dataRes.data || [])) {
const r = row.get ? row.get({ plain: true }) : row;
const regionKey = deviceRegionMap.get(r.device_id);
if (!regionKey || regionEnvMap.has(regionKey)) continue;
regionEnvMap.set(regionKey, parseDeviceData(r.device_data));
}
if (regionEnvMap.size === 0) return;
// 5. 逐台空调执行联动控制(串行,避免飞奕接口压力)
for (const ac of acDevices) {
await controlSingleAc(ac, linkageMap.get(ac.device_id), regionEnvMap.get(ac.region_key));
}
} catch (err) {
console.error('[空调联动控制] 执行异常:', err);
}
}
/**
* 单台空调联动控制
* @param ac 空调设备(含 device_id / device_name / region_key)
* @param linkage 该设备有效联动配置,无配置则跳过
* @param env 该区域最新环境数据(temperature/co2 等),无数据则跳过
*/
async function controlSingleAc(ac: any, linkage: any, env: any): Promise<void> {
if (!linkage || !env) return;
const deviceId = ac.device_id;
const type = linkage.linkage_type || '';
if (!WORK_MODE_FOR_LINKAGE[type]) return;
// 计算目标状态
let targetTemp: number | null = null;
let targetFanText: string | null = null;
let envValue: number | null = null;
if (type === '新风') {
envValue = toNumber(env['co2']);
if (envValue === null) return;
const tMax = toNumber(linkage.threshold_max);
const tMin = toNumber(linkage.threshold_min);
if (tMax === null || tMin === null) return;
if (envValue > tMax) targetFanText = linkage.fan_speed_max || '高风';
else if (envValue < tMin) targetFanText = linkage.fan_speed_min || '低风';
else targetFanText = linkage.fan_speed_mid || '中风';
} else {
envValue = toNumber(env['temperature']);
if (envValue === null) return;
const tMax = toNumber(linkage.threshold_max);
const tMin = toNumber(linkage.threshold_min);
if (tMax === null || tMin === null) return;
if (type === '制冷') {
if (envValue >= tMax) {
targetTemp = toNumber(linkage.setting_max);
targetFanText = linkage.fan_speed_max || '高风';
} else if (envValue < tMin) {
targetTemp = toNumber(linkage.setting_min);
targetFanText = linkage.fan_speed_min || '低风';
} else {
targetTemp = toNumber(linkage.setting_mid);
targetFanText = linkage.fan_speed_mid || '中风';
}
} else { // 制热
if (envValue > tMax) {
targetTemp = toNumber(linkage.setting_max);
targetFanText = linkage.fan_speed_max || '高风';
} else if (envValue <= tMin) {
targetTemp = toNumber(linkage.setting_min);
targetFanText = linkage.fan_speed_min || '低风';
} else {
targetTemp = toNumber(linkage.setting_mid);
targetFanText = linkage.fan_speed_mid || '中风';
}
}
}
if (targetFanText === null) return;
const targetWorkMode = WORK_MODE_FOR_LINKAGE[type];
const targetFanNum = fanSpeedMap[targetFanText];
if (targetFanNum === undefined) return;
// 目标状态:任何区间均保持开机
const newTarget = {
power: '开',
temp: targetTemp,
fan: targetFanText,
mode: WORK_MODE_TEXT_MAP[targetWorkMode] || type,
};
// 读取上次联动状态
let last: any = null;
try {
const stateRes = await selectOneDataByParam(TABLENAME.设备联动状态表, { device_id: deviceId });
last = stateRes.data || null;
} catch (e) {
last = null;
}
const lastTarget = last ? {
power: last.target_power,
temp: last.target_set_temp ? Number(last.target_set_temp) : null,
fan: last.target_fan_speed,
mode: last.target_work_mode,
} : null;
// 判断是否需要下发
let needSend = false;
let reason = '';
if (!lastTarget) {
needSend = true;
reason = '首次联动';
} else if (
lastTarget.power !== newTarget.power ||
lastTarget.temp !== newTarget.temp ||
lastTarget.fan !== newTarget.fan ||
lastTarget.mode !== newTarget.mode
) {
needSend = true;
reason = '环境数据跨越阈值';
} else {
// 目标未变化:查询飞奕实际状态,检测是否被人为修改
try {
const real = await getIndoorUnitStatus(deviceId);
const realPower = real.power === 1 ? '开' : '关';
const realTemp = real.setTemp !== undefined && real.setTemp !== null ? Number(real.setTemp) : null;
const realFan = FAN_SPEED_TEXT_MAP[real.fanSpeed] || null;
const realMode = WORK_MODE_TEXT_MAP[real.mode] || null;
if (
realPower !== newTarget.power ||
(newTarget.temp !== null && realTemp !== newTarget.temp) ||
(newTarget.fan !== null && realFan !== newTarget.fan) ||
(newTarget.mode !== null && realMode !== newTarget.mode)
) {
needSend = true;
reason = '空调状态被人为修改,纠正回联动目标';
}
} catch (e) {
console.warn(`[空调联动控制] 查询设备 ${deviceId} 实际状态失败,本轮跳过`, e);
}
}
if (!needSend) return;
// 组装下发参数:每次均传 workMode,任何区间均开机
const acParams: any = {
indoorUnitAddressFull: [deviceId],
workMode: targetWorkMode,
onOff: powerMap[newTarget.power],
tempSet: targetTemp,
fanSpeed: targetFanNum,
openApiAction: 'control',
};
if (targetTemp === null) delete acParams.tempSet;
// 下发控制(失败不抛异常,记录日志)
let result: any = { success: false, data: '未知错误' };
try {
// 3. 调用飞奕控制接口
// result = await controlIndoorUnit(acParams);
} catch (e: any) {
result = { success: false, data: e?.message || '控制接口异常' };
}
// 记录日志:成功/失败均记录,包含详细传参
const now = new Date();
try {
await addData(TABLENAME.设备日志表, [{
device_id: deviceId,
device_type: '空调',
operation_content: {
option: '联动控制',
device_id: deviceId,
time: formatLocalTime(now),
linkage_type: type,
env_value: envValue,
reason,
params: acParams,
success: result.success,
message: result.data || (result.success ? '控制成功' : '控制失败'),
},
status: result.success ? 1 : 0,
created_at: now,
updated_at: now,
}]);
} catch (e) {
console.error(`[空调联动控制] 记录日志失败,设备 ${deviceId}:`, e);
}
// 下发成功才更新联动状态,失败保留旧状态以便下轮重试
if (result.success) {
const upsert = {
target_power: newTarget.power,
target_set_temp: targetTemp !== null ? String(targetTemp) : null,
target_fan_speed: newTarget.fan,
target_work_mode: newTarget.mode,
update_time: now,
};
try {
if (last && last.id) {
await updateManyData(TABLENAME.设备联动状态表, { device_id: deviceId }, { ...upsert, updated_at: now });
} else {
await addData(TABLENAME.设备联动状态表, { device_id: deviceId, ...upsert, created_at: now, updated_at: now });
}
} catch (e) {
console.error(`[空调联动控制] 更新联动状态失败,设备 ${deviceId}:`, e);
}
}
}
/** 安全转数字,非法返回 null */
function toNumber(v: any): number | null {
if (v === null || v === undefined || v === '') return null;
const n = Number(v);
return isNaN(n) ? null : n;
}
/** 解析 device_data JSON 字段 */
function parseDeviceData(json: any): any {
if (!json) return {};
if (typeof json === 'object') return json;
try {
return JSON.parse(json);
} catch (e) {
return {};
}
}
...@@ -11,11 +11,11 @@ ...@@ -11,11 +11,11 @@
* // 热湿环境 * // 热湿环境
* rshj: { pmv: "+0.28", ppd: "5.6%", wd: "22.5℃", sd: "50%", pmvTrend: [...] } * rshj: { pmv: "+0.28", ppd: "5.6%", wd: "22.5℃", sd: "50%", pmvTrend: [...] }
* // 空气品质 * // 空气品质
* kqpz: { co2: "0.28", pm25: "5.6", tvoc: "22.5", pm10: "50", kqpzTrend: [...] } * kqpz: { co2: "0.28ppm", pm25: "5.6μg/m³", tvoc: "22.5μg/m³", pm10: "50μg/m³", kqpzTrend: [...] }
* // 光环境 * // 光环境
* ghj: { lux: "+0.28", ugr: "5.6%", ghjTrend: [...] } * ghj: { lux: "+0.28lux", ugr: "5.6%", ghjTrend: [...] }
* // 声环境 * // 声环境
* shj: { dB: "28", nr: "NR-222", maxdB: "56", shjTrend: [...] } * shj: { dB: "28dB", nr: "NR-222", maxdB: "56dB", shjTrend: [...] }
* // 区域综合舒适度 * // 区域综合舒适度
* qyzhssd: [{ regionName: "办公室", pmv: "0.28", co2: "0.28", lux: "0.28", dB: "0.28", zhssd: "28", zb: "28%" }, ...] * qyzhssd: [{ regionName: "办公室", pmv: "0.28", co2: "0.28", lux: "0.28", dB: "0.28", zhssd: "28", zb: "28%" }, ...]
* // 实时异常监控 * // 实时异常监控
...@@ -319,7 +319,7 @@ async function getRegionData(): Promise<Array<{ ...@@ -319,7 +319,7 @@ async function getRegionData(): Promise<Array<{
for (const row of latestData) { for (const row of latestData) {
const rk = deviceRegionMap[row.device_id]; const rk = deviceRegionMap[row.device_id];
if (!rk || !row.device_data) continue; if (!rk || !row.device_data) continue;
if (!regionAgg[rk]) regionAgg[rk] = { temp: [], hum: [], co2: [], lux: [], a14263: [] }; if (!regionAgg[rk]) regionAgg[rk] = { temp: [], hum: [], co2: [], lux: [], a14263: [] };
const d = row.device_data; const d = row.device_data;
if (d.temperature !== undefined) regionAgg[rk].temp.push(d.temperature); if (d.temperature !== undefined) regionAgg[rk].temp.push(d.temperature);
if (d.humidity !== undefined) regionAgg[rk].hum.push(d.humidity); if (d.humidity !== undefined) regionAgg[rk].hum.push(d.humidity);
...@@ -529,10 +529,10 @@ export async function getCustomsAnalysis() { ...@@ -529,10 +529,10 @@ export async function getCustomsAnalysis() {
pmvTrend: pmvTrendSliced, pmvTrend: pmvTrendSliced,
}, },
kqpz: { kqpz: {
co2: sensorData.co2.toFixed(2), co2: sensorData.co2.toFixed(2) + 'ppm',
pm25: sensorData.pm25.toFixed(2), pm25: sensorData.pm25.toFixed(2) + 'μg/m³',
tvoc: sensorData.tvoc.toFixed(2), tvoc: sensorData.tvoc.toFixed(2) + 'μg/m³',
pm10: (sensorData.pm10 || 0).toFixed(2), pm10: (sensorData.pm10 || 0).toFixed(2) + 'μg/m³',
kqpzTrend: airTrendSliced, kqpzTrend: airTrendSliced,
}, },
ghj: { ghj: {
...@@ -541,9 +541,9 @@ export async function getCustomsAnalysis() { ...@@ -541,9 +541,9 @@ export async function getCustomsAnalysis() {
ghjTrend: lightTrendSliced, ghjTrend: lightTrendSliced,
}, },
shj: { shj: {
dB: sensorData.a14263.toFixed(2), dB: sensorData.a14263.toFixed(2) + 'dB',
nr: 'NR-' + Math.round(nr), nr: 'NR-' + Math.round(nr),
maxdB: maxNoise.toFixed(2), maxdB: maxNoise.toFixed(2) + 'dB',
shjTrend: noiseTrendSliced, shjTrend: noiseTrendSliced,
}, },
qyzhssd: regionData, qyzhssd: regionData,
......
...@@ -20,17 +20,17 @@ export const STANDARDS = { ...@@ -20,17 +20,17 @@ export const STANDARDS = {
}; };
/** PMV 舒适区间 [-0.5, +0.5] */ /** PMV 舒适区间 [-0.5, +0.5] */
const PMV_COMFORT_MIN = -0.5; export const PMV_COMFORT_MIN = -0.5;
const PMV_COMFORT_MAX = 0.5; export const PMV_COMFORT_MAX = 0.5;
/** PPD 限值 ≤ 20% */ /** PPD 限值 ≤ 20% */
const PPD_LIMIT = 20; export const PPD_LIMIT = 20;
/** UGR 展厅限值 ≤ 22 */ /** UGR 展厅限值 ≤ 22 */
const UGR_LIMIT = 22; export const UGR_LIMIT = 22;
/** NR 办公场所限值 ≤ 40 */ /** NR 办公场所限值 ≤ 40 */
const NR_LIMIT = 40; export const NR_LIMIT = 40;
/** 噪音 24h 峰值告警阈值 > 65 dB(A) */ /** 噪音 24h 峰值告警阈值 > 65 dB(A) */
export const NOISE_ALERT_THRESHOLD = 65; export const NOISE_ALERT_THRESHOLD = 65;
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
getDataSourceDetail, getDataSourceDetail,
} from "./diqinClient"; } from "./diqinClient";
import { mysqlModelMap } from "../model/sqlModelBind"; import { mysqlModelMap } from "../model/sqlModelBind";
import { acLinkageControl } from "./acLinkage";
import { systemConfig } from "../config/serverConfig"; import { systemConfig } from "../config/serverConfig";
import Sequelize from "sequelize"; import Sequelize from "sequelize";
import { DEVICE_STATE, WORK_MODE_TEXT_MAP, FAN_SPEED_TEXT_MAP } from "../config/businessEnum"; import { DEVICE_STATE, WORK_MODE_TEXT_MAP, FAN_SPEED_TEXT_MAP } from "../config/businessEnum";
...@@ -1645,6 +1646,8 @@ export async function diqinSyncAll() { ...@@ -1645,6 +1646,8 @@ export async function diqinSyncAll() {
await diqinRegionDeviceRel(); await diqinRegionDeviceRel();
await diqinDeviceRegionKey(); await diqinDeviceRegionKey();
await diqinDeviceData(); await diqinDeviceData();
// 环境数据同步后触发空调自动联动控制
await acLinkageControl();
console.log('[迪勤全量同步] 完成'); console.log('[迪勤全量同步] 完成');
} }
......
...@@ -283,7 +283,8 @@ export async function editAcDeviceControl(params: { ...@@ -283,7 +283,8 @@ export async function editAcDeviceControl(params: {
if (deviceMode) acParams.workMode = workModeMap[deviceMode]; if (deviceMode) acParams.workMode = workModeMap[deviceMode];
// 3. 调用飞奕控制接口 // 3. 调用飞奕控制接口
const result = await controlIndoorUnit(acParams); const result = { success:false,data:{} };
// result = await controlIndoorUnit(acParams);
// 4. 记录操作日志 // 4. 记录操作日志
const now = new Date(); const now = new Date();
...@@ -296,9 +297,9 @@ export async function editAcDeviceControl(params: { ...@@ -296,9 +297,9 @@ export async function editAcDeviceControl(params: {
device_id: deviceId, device_id: deviceId,
time: formatLocalTime(now), time: formatLocalTime(now),
power: power, power: power,
setTemp: setTemp, set_temp: setTemp ?? '',
fanSpeed: fanSpeed, fan_speed: fanSpeed ?? '',
deviceMode: deviceMode, device_mode: deviceMode ?? '',
success: result.success, success: result.success,
message: result.data || (result.success ? '控制成功' : '控制失败'), message: result.data || (result.success ? '控制成功' : '控制失败'),
}, },
...@@ -622,7 +623,7 @@ export async function batchEditLocalDevices(params: { ...@@ -622,7 +623,7 @@ export async function batchEditLocalDevices(params: {
deviceName?: string; deviceName?: string;
regionKey?: number; regionKey?: number;
deviceType?: string; deviceType?: string;
controlParams?: string; linkageStart?: string;
deviceState?: string; deviceState?: string;
}; };
}) { }) {
...@@ -645,7 +646,7 @@ export async function batchEditLocalDevices(params: { ...@@ -645,7 +646,7 @@ export async function batchEditLocalDevices(params: {
if (edits.deviceName !== undefined) dbEdits.device_name = edits.deviceName; if (edits.deviceName !== undefined) dbEdits.device_name = edits.deviceName;
if (edits.regionKey !== undefined) dbEdits.region_key = edits.regionKey; if (edits.regionKey !== undefined) dbEdits.region_key = edits.regionKey;
if (edits.deviceType !== undefined) dbEdits.device_type = edits.deviceType; if (edits.deviceType !== undefined) dbEdits.device_type = edits.deviceType;
if (edits.controlParams !== undefined) dbEdits.device_param = edits.controlParams; if (edits.linkageStart !== undefined) dbEdits.linkage_start = edits.linkageStart;
if (edits.deviceState !== undefined) dbEdits.device_state = edits.deviceState; if (edits.deviceState !== undefined) dbEdits.device_state = edits.deviceState;
if (Object.keys(dbEdits).length === 0) { if (Object.keys(dbEdits).length === 0) {
......
...@@ -15,6 +15,17 @@ import { delData } from '../data/delData'; ...@@ -15,6 +15,17 @@ import { delData } from '../data/delData';
import { mysqlModelMap } from '../model/sqlModelBind'; import { mysqlModelMap } from '../model/sqlModelBind';
import { TABLENAME } from '../config/dbEnum'; import { TABLENAME } from '../config/dbEnum';
import { formatTime } from '../util/dateUtils'; import { formatTime } from '../util/dateUtils';
import {
STANDARDS,
isCompliant,
calcPMV,
calcPPD,
calcNR,
PMV_COMFORT_MIN,
PMV_COMFORT_MAX,
PPD_LIMIT,
NR_LIMIT,
} from './customsIndicators';
const xlsx = require('node-xlsx'); const xlsx = require('node-xlsx');
...@@ -142,6 +153,119 @@ function judgeFourIndicators( ...@@ -142,6 +153,119 @@ function judgeFourIndicators(
return levels[worst - 1]; return levels[worst - 1];
} }
// ==================== 区域四维度达标 targets ====================
/** 单维度达标项类型 */
export type RegionTargetItem = {
name: string; // 维度名:热湿环境 / 空气品质 / 光环境 / 声环境
rate: string; // 达标率,如 "75%";无数据时为 "--"
level: number; // 1优 2良 3中 4差;无数据时为 0
};
/** IEQ 传感器上报的环境指标 key */
const ENV_METRICS = [
'temperature', 'humidity', 'co2', 'pm25', 'tvoc', 'a31001', 'lightLevel', 'a14263',
] as const;
type EnvMetric = (typeof ENV_METRICS)[number];
/** 区域环境指标值(各指标取该区域下设备最新数据均值) */
type RegionEnvValues = Partial<Record<EnvMetric, number>>;
/**
* 达标率(%) → 等级:≥90=1优,≥75=2良,≥60=3中,其余=4差
* 与 calcOverallComfort 的 90/75/60 分档保持一致
*/
function rateToLevel(rate: number): number {
if (rate >= 90) return 1;
if (rate >= 75) return 2;
if (rate >= 60) return 3;
return 4;
}
/** 汇总单维度达标项占比 */
function summarizeTarget(name: string, passed: number, total: number): RegionTargetItem {
if (total <= 0) return { name, rate: '--', level: 0 };
const rate = Math.round((passed / total) * 100);
return { name, rate: rate + '%', level: rateToLevel(rate) };
}
/**
* 根据区域最新环境数据计算四维度达标率 targets
* 口径:各维度内部逐项判达标,达标项占比作为达标率
* - 热湿环境:温度[18,28]、湿度[40,70]、PMV[-0.5,0.5]、PPD≤20
* - 空气品质:CO₂≤1000、PM2.5≤75、TVOC≤0.6、甲醛(a31001)≤0.1
* - 光环境:照度 lightLevel∈[200,2000]
* - 声环境:声级 a14263≤60、NR≤40(NR 由声级估算)
*/
function buildRegionTargets(env: RegionEnvValues): RegionTargetItem[] {
const temp = env.temperature;
const hum = env.humidity;
// 热湿环境
let thermalPassed = 0;
let thermalTotal = 0;
if (temp !== undefined) {
thermalTotal++;
if (isCompliant(temp, STANDARDS.temperature)) thermalPassed++;
}
if (hum !== undefined) {
thermalTotal++;
if (isCompliant(hum, STANDARDS.humidity)) thermalPassed++;
}
if (temp !== undefined && hum !== undefined) {
// 温度和湿度齐备时才计算 PMV / PPD
const pmv = calcPMV(temp, hum);
const ppd = calcPPD(pmv);
thermalTotal += 2;
if (pmv >= PMV_COMFORT_MIN && pmv <= PMV_COMFORT_MAX) thermalPassed++;
if (ppd <= PPD_LIMIT) thermalPassed++;
}
// 空气品质
let airPassed = 0;
let airTotal = 0;
if (env.co2 !== undefined) {
airTotal++;
if (isCompliant(env.co2, STANDARDS.co2)) airPassed++;
}
if (env.pm25 !== undefined) {
airTotal++;
if (isCompliant(env.pm25, STANDARDS.pm25)) airPassed++;
}
if (env.tvoc !== undefined) {
airTotal++;
if (isCompliant(env.tvoc, STANDARDS.tvoc)) airPassed++;
}
if (env.a31001 !== undefined) {
airTotal++;
if (isCompliant(env.a31001, STANDARDS.hcho)) airPassed++;
}
// 光环境:照度是否达标(区域单点)
let lightPassed = 0;
let lightTotal = 0;
if (env.lightLevel !== undefined) {
lightTotal++;
if (isCompliant(env.lightLevel, STANDARDS.lightLevel)) lightPassed++;
}
// 声环境:声级 + NR
let soundPassed = 0;
let soundTotal = 0;
if (env.a14263 !== undefined) {
soundTotal += 2;
if (isCompliant(env.a14263, STANDARDS.noise)) soundPassed++;
if (calcNR(env.a14263) <= NR_LIMIT) soundPassed++;
}
return [
summarizeTarget('热湿', thermalPassed, thermalTotal),
summarizeTarget('空气', airPassed, airTotal),
summarizeTarget('照度', lightPassed, lightTotal),
summarizeTarget('噪音', soundPassed, soundTotal),
];
}
/** /**
* 动态查询:根据 regionName→regionKey 映射,从数据库查询各区域下的设备及最新功率状态 * 动态查询:根据 regionName→regionKey 映射,从数据库查询各区域下的设备及最新功率状态
* 同时提取最新 co2/pm25 数据计算各区域环境质量 * 同时提取最新 co2/pm25 数据计算各区域环境质量
...@@ -155,10 +279,12 @@ async function buildDeviceMapDynamic( ...@@ -155,10 +279,12 @@ async function buildDeviceMapDynamic(
): Promise<{ ): Promise<{
deviceMap: { [regionName: string]: any[] }; deviceMap: { [regionName: string]: any[] };
qualityMap: { [regionName: string]: string }; qualityMap: { [regionName: string]: string };
targetsMap: { [regionName: string]: RegionTargetItem[] };
dataMap: { [deviceId: string]: any }; dataMap: { [deviceId: string]: any };
}> { }> {
const deviceMap: { [regionName: string]: any[] } = {}; const deviceMap: { [regionName: string]: any[] } = {};
const qualityMap: { [regionName: string]: string } = {}; const qualityMap: { [regionName: string]: string } = {};
const targetsMap: { [regionName: string]: RegionTargetItem[] } = {};
const dataMap: { [deviceId: string]: any } = {}; const dataMap: { [deviceId: string]: any } = {};
// 0. 根据 sheet 解析出的设备编号(已剔除占位符)查询设备表,构建 device_id → 设备信息映射 // 0. 根据 sheet 解析出的设备编号(已剔除占位符)查询设备表,构建 device_id → 设备信息映射
...@@ -175,7 +301,7 @@ async function buildDeviceMapDynamic( ...@@ -175,7 +301,7 @@ async function buildDeviceMapDynamic(
// 1. 收集所有 region_key // 1. 收集所有 region_key
const regionKeys = Object.values(regionMap) as string[]; const regionKeys = Object.values(regionMap) as string[];
if (regionKeys.length === 0) return { deviceMap, qualityMap, dataMap }; if (regionKeys.length === 0) return { deviceMap, qualityMap, targetsMap, dataMap };
// 2. 反向映射:regionKey → regionName // 2. 反向映射:regionKey → regionName
const regionKeyToName: { [key: string]: string } = {}; const regionKeyToName: { [key: string]: string } = {};
...@@ -195,10 +321,10 @@ async function buildDeviceMapDynamic( ...@@ -195,10 +321,10 @@ async function buildDeviceMapDynamic(
const deviceIds = devices.map((d) => d.device_id).filter(Boolean); const deviceIds = devices.map((d) => d.device_id).filter(Boolean);
const powerMap = new Map<string, string>(); const powerMap = new Map<string, string>();
const deviceDataMap = new Map<string, any>(); // device_id → 最新 device_data 解析对象 const deviceDataMap = new Map<string, any>(); // device_id → 最新 device_data 解析对象
// 按区域收集 co2 / pm25 值 // 按区域收集环境指标值(每台设备最新一条,多设备取均值)
const regionEnvMap: { [regionName: string]: { co2List: number[]; pm25List: number[] } } = {}; const regionEnvMap: { [regionName: string]: Partial<Record<EnvMetric, number[]>> } = {};
for (const name of Object.keys(regionMap)) { for (const name of Object.keys(regionMap)) {
regionEnvMap[name] = { co2List: [], pm25List: [] }; regionEnvMap[name] = {};
} }
if (deviceIds.length > 0) { if (deviceIds.length > 0) {
// 计算时间截断点:当前时间减去 hour 小时(不截断整点) // 计算时间截断点:当前时间减去 hour 小时(不截断整点)
...@@ -237,12 +363,17 @@ async function buildDeviceMapDynamic( ...@@ -237,12 +363,17 @@ async function buildDeviceMapDynamic(
powerMap.set(r.device_id, parsed?.power || 'on'); powerMap.set(r.device_id, parsed?.power || 'on');
deviceDataMap.set(r.device_id, parsed); deviceDataMap.set(r.device_id, parsed);
// 提取环境数据 // 提取环境数据(仅收集区域内有效数值型指标)
const rk = devices.find(d => d.device_id === r.device_id)?.region_key; const rk = devices.find(d => d.device_id === r.device_id)?.region_key;
const rn = rk ? regionKeyToName[rk] : undefined; const rn = rk ? regionKeyToName[rk] : undefined;
if (rn && regionEnvMap[rn]) { if (rn && regionEnvMap[rn]) {
if (typeof parsed?.co2 === 'number') regionEnvMap[rn].co2List.push(parsed.co2); for (const metric of ENV_METRICS) {
if (typeof parsed?.pm25 === 'number') regionEnvMap[rn].pm25List.push(parsed.pm25); const val = (parsed as any)?.[metric];
if (typeof val === 'number') {
const arr = (regionEnvMap[rn][metric] = regionEnvMap[rn][metric] || []);
arr.push(val);
}
}
} }
}); });
} }
...@@ -307,16 +438,28 @@ async function buildDeviceMapDynamic( ...@@ -307,16 +438,28 @@ async function buildDeviceMapDynamic(
} }
} }
// 6. 根据各区域环境数据计算空气质量等级 // 6. 根据各区域环境数据计算空气质量等级 + 四维度 targets
for (const [name, entry] of Object.entries(regionEnvMap)) { for (const name of Object.keys(regionMap)) {
const avgCo2 = entry.co2List.length > 0 const entry = regionEnvMap[name];
? entry.co2List.reduce((a, b) => a + b, 0) / entry.co2List.length : 0; // 计算各指标均值(无数据时为 undefined)
const avgPm25 = entry.pm25List.length > 0 const avgOf = (metric: EnvMetric): number | undefined => {
? entry.pm25List.reduce((a, b) => a + b, 0) / entry.pm25List.length : 0; const list = entry[metric];
qualityMap[name] = judgeQuality(avgCo2, avgPm25, entry.co2List.length > 0, entry.pm25List.length > 0); if (!list || list.length === 0) return undefined;
return list.reduce((a, b) => a + b, 0) / list.length;
};
const avgCo2 = avgOf('co2') ?? 0;
const avgPm25 = avgOf('pm25') ?? 0;
qualityMap[name] = judgeQuality(avgCo2, avgPm25, avgOf('co2') !== undefined, avgOf('pm25') !== undefined);
const envValues: RegionEnvValues = {};
for (const metric of ENV_METRICS) {
envValues[metric] = avgOf(metric);
}
targetsMap[name] = buildRegionTargets(envValues);
} }
return { deviceMap, qualityMap, dataMap }; return { deviceMap, qualityMap, targetsMap, dataMap };
} }
// ================================== 读取区域坐标分布.xlsx ============================================ // ================================== 读取区域坐标分布.xlsx ============================================
...@@ -399,7 +542,7 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) { ...@@ -399,7 +542,7 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
} }
} }
} }
const { deviceMap, qualityMap, dataMap } = await buildDeviceMapDynamic(regionMap, ieqTagMap, hour, deviceIdMap); const { deviceMap, qualityMap, targetsMap, dataMap } = await buildDeviceMapDynamic(regionMap, ieqTagMap, hour, deviceIdMap);
let sheetData = await planaryArrayBecomeOfBlockData(sheet); let sheetData = await planaryArrayBecomeOfBlockData(sheet);
let result = []; let result = [];
if (sheetData && sheetData[0].blockData) { if (sheetData && sheetData[0].blockData) {
...@@ -419,7 +562,7 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) { ...@@ -419,7 +562,7 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
deviceId: row[dindex], deviceId: row[dindex],
power: row[dindex] ? (dataMap[row[dindex]]?.device_state && dataMap[row[dindex]]?.device_state === '1' ? 'off' : 'on') : undefined, power: row[dindex] ? (dataMap[row[dindex]]?.device_state && dataMap[row[dindex]]?.device_state === '1' ? 'off' : 'on') : undefined,
devices: deviceMap[row[0]], devices: deviceMap[row[0]],
targets: [] targets: targetsMap[row[0]] ?? []
}))?? []; }))?? [];
} }
return result; return result;
......
...@@ -283,6 +283,9 @@ function parseDeviceData(raw: any): any { ...@@ -283,6 +283,9 @@ function parseDeviceData(raw: any): any {
// TODO: 电表数据接入后,确认 device_data JSON 中电量字段的 key,目前暂定 'current' // TODO: 电表数据接入后,确认 device_data JSON 中电量字段的 key,目前暂定 'current'
const ELECTRICITY_KEY = 'current'; const ELECTRICITY_KEY = 'current';
/** 区域用电逐日趋势展示最近 N 天(region_expenses 区域电费表按天聚合) */
const ELECTRICITY_TREND_DAYS = 14;
/** /**
* 查询电表设备在指定时间范围内的电量总和 * 查询电表设备在指定时间范围内的电量总和
*/ */
...@@ -1232,47 +1235,91 @@ export async function getAnalysisPopup(regionKey: string) { ...@@ -1232,47 +1235,91 @@ export async function getAnalysisPopup(regionKey: string) {
} }
sbztjcqs.sort((a, b) => a.key.localeCompare(b.key)); sbztjcqs.sort((a, b) => a.key.localeCompare(b.key));
// 能耗监测(该区域电表24小时趋势) // 能耗监测(该区域用电量趋势,数据源:region_expenses 区域电费表,按天一条)
let nhjc: { key: string; value: string }[] = []; let nhjc: { key: string; value: string }[] = [];
if (deviceIds.length > 0) { {
const meterIds = devices.filter((d: any) => d.device_type === '电表').map((d: any) => d.device_id); const endDate = formatTime(new Date()).split(' ')[0];
if (meterIds.length > 0) { const startDt = new Date();
const meterDataResult = await selectDataListByParam( startDt.setDate(startDt.getDate() - (ELECTRICITY_TREND_DAYS - 1));
const startDate = `${startDt.getFullYear()}-${String(startDt.getMonth() + 1).padStart(2, '0')}-${String(startDt.getDate()).padStart(2, '0')}`;
const expenseResult = await selectDataListByParam(
TABLENAME.区域电费表,
{
region_key: regionKey,
expense_date: { '%gte%': startDate, '%lte%': endDate },
'%orderAsc%': 'expense_date',
},
['expense_date', 'use_power']
);
nhjc = (expenseResult.data || []).map((r: any) => ({
key: r.expense_date,
value: (Number(r.use_power) || 0).toFixed(2),
}));
}
// ====== MOCK DATA(保留,勿删) ======
// 环境监测
// let hjjc:any = {
// wdhjjc: [{ key: "2026-08-10 08:00:00", value: 10 }],
// sdhjjc: [{ key: "2026-08-10 08:00:00", value: 20 }],
// pm25hjjc: [{ key: "2026-08-10 08:00:00", value: 30 }],
// co2hjjc: [{ key: "2026-08-10 08:00:00", value: 40 }],
// pm10hjjc: [{ key: "2026-08-10 08:00:00", value: 50 }],
// tvochjjc: [{ key: "2026-08-10 08:00:00", value: 60 }],
// };
// ====== MOCK DATA END ====== */
// ---- 真实查询 ----
// 环境监测(该区域 IEQ 传感器近 24 小时趋势:每小时取最新一条)
const ENV_MONITOR_MAP: Record<string, string> = {
wdhjjc: 'temperature',
sdhjjc: 'humidity',
pm25hjjc: 'pm25',
co2hjjc: 'co2',
pm10hjjc: 'pm10',
tvochjjc: 'tvoc',
};
let hjjc: any = {};
{
const ieqIds = devices.filter((d: any) => d.device_type === 'IEQ传感器').map((d: any) => d.device_id);
if (ieqIds.length > 0) {
const envDataResult = await selectDataListByParam(
TABLENAME.设备数据表, TABLENAME.设备数据表,
{ {
device_id: { '%in%': meterIds }, device_id: { '%in%': ieqIds },
device_time: { '%gte%': trendStart, '%lte%': todayEnd }, device_time: { '%gte%': trendStart, '%lte%': todayEnd },
'%orderAsc%': 'device_time', '%orderAsc%': 'device_time',
}, },
['device_data', 'device_time'] ['device_data', 'device_time']
); );
const hourlySum = new Map<string, number>(); // 按小时取最新一条:device_time 升序遍历,后写覆盖
(meterDataResult.data || []).forEach((r: any) => { const hourlyMap = new Map<string, Map<string, number>>();
(envDataResult.data || []).forEach((r: any) => {
const t = new Date(r.device_time); const t = new Date(r.device_time);
const h = String(t.getHours()).padStart(2, '0'); const hourKey = `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')} ${String(t.getHours()).padStart(2, '0')}:00`;
const hourKey = todayStart.split(' ')[0] + ' ' + h + ':00';
const parsed = parseDeviceData(r.device_data); const parsed = parseDeviceData(r.device_data);
const val = parsed[ELECTRICITY_KEY]; if (!parsed) return;
if (typeof val === 'number') { if (!hourlyMap.has(hourKey)) hourlyMap.set(hourKey, new Map());
hourlySum.set(hourKey, (hourlySum.get(hourKey) || 0) + val); const hourData = hourlyMap.get(hourKey)!;
Object.values(ENV_MONITOR_MAP).forEach((indicator) => {
const val = parsed[indicator];
if (val !== undefined && val !== null && typeof val === 'number') {
hourData.set(indicator, val);
} }
}); });
nhjc = Array.from(hourlySum.entries()) });
.map(([key, value]) => ({ key, value: String(Math.round(value)) })) // 组装六项输出(缺数据的指标输出空数组)
Object.entries(ENV_MONITOR_MAP).forEach(([outKey, indicator]) => {
hjjc[outKey] = Array.from(hourlyMap.entries())
.filter(([, m]) => m.has(indicator))
.map(([key, m]) => ({ key, value: m.get(indicator) }))
.sort((a, b) => a.key.localeCompare(b.key)); .sort((a, b) => a.key.localeCompare(b.key));
});
} else {
Object.keys(ENV_MONITOR_MAP).forEach((outKey) => { hjjc[outKey] = []; });
} }
} }
// 环境监测(该区域电表24小时趋势)
let hjjc:any = {
wdhjjc: [{ key: "2026-08-10 08:00:00", value: 10 }],
sdhjjc: [{ key: "2026-08-10 08:00:00", value: 20 }],
pm25hjjc: [{ key: "2026-08-10 08:00:00", value: 30 }],
co2hjjc: [{ key: "2026-08-10 08:00:00", value: 40 }],
pm10hjjc: [{ key: "2026-08-10 08:00:00", value: 50 }],
tvochjjc: [{ key: "2026-08-10 08:00:00", value: 60 }],
};
return { return {
"regionName": regionName, "regionName": regionName,
"sbztjc": sbztjc, "sbztjc": sbztjc,
...@@ -1307,26 +1354,30 @@ export async function getAnalysisPopupSubTrend(currRegionKey: string, selectedRe ...@@ -1307,26 +1354,30 @@ export async function getAnalysisPopupSubTrend(currRegionKey: string, selectedRe
====== MOCK DATA END ====== */ ====== MOCK DATA END ====== */
// ---- 真实查询 ---- // ---- 真实查询 ----
// 时间范围:过去 24 小时 ~ 当前时刻(只输出到当前小时,不查未来) // 能耗:过去 N 天(日期粒度);环境:过去 24 小时(只输出到当前小时,不查未来)
const trendStart = getHoursAgo(24); const trendStart = getHoursAgo(24);
const trendEnd = formatTime(new Date()); const trendEnd = formatTime(new Date());
const expenseEndDate = formatTime(new Date()).split(' ')[0];
// 两区域分别聚合:能耗(电表按小时求和)、环境(IEQ 按小时取最新一条) const expenseStartDt = new Date();
const [currMeterHourly, selMeterHourly] = await Promise.all([ expenseStartDt.setDate(expenseStartDt.getDate() - (ELECTRICITY_TREND_DAYS - 1));
queryRegionMeterHourly(currRegionKey, trendStart, trendEnd), const expenseStartDate = `${expenseStartDt.getFullYear()}-${String(expenseStartDt.getMonth() + 1).padStart(2, '0')}-${String(expenseStartDt.getDate()).padStart(2, '0')}`;
queryRegionMeterHourly(selectedRegionKey, trendStart, trendEnd),
// 两区域分别聚合:能耗(region_expenses 区域电费表按天)、环境(IEQ 按小时取最新一条)
const [currExpenseDaily, selExpenseDaily] = await Promise.all([
queryRegionExpenseDaily(currRegionKey, expenseStartDate, expenseEndDate),
queryRegionExpenseDaily(selectedRegionKey, expenseStartDate, expenseEndDate),
]); ]);
const [currIeqHourly, selIeqHourly] = await Promise.all([ const [currIeqHourly, selIeqHourly] = await Promise.all([
queryRegionIeqHourly(currRegionKey, trendStart, trendEnd), queryRegionIeqHourly(currRegionKey, trendStart, trendEnd),
queryRegionIeqHourly(selectedRegionKey, trendStart, trendEnd), queryRegionIeqHourly(selectedRegionKey, trendStart, trendEnd),
]); ]);
// 能耗对比:hourKey 取两区域并集,缺数据侧输出 null(不补零) // 能耗对比:日期 key 取两区域并集,缺数据侧输出 null(不补零)
const nhjcHourKeys = Array.from(new Set([...currMeterHourly.keys(), ...selMeterHourly.keys()])).sort(); const nhjcDateKeys = Array.from(new Set([...currExpenseDaily.keys(), ...selExpenseDaily.keys()])).sort();
let nhjcdb: { key: string; dqqy: number | null, dbqy: number | null }[] = nhjcHourKeys.map((key) => ({ let nhjcdb: { key: string; dqqy: number | null, dbqy: number | null }[] = nhjcDateKeys.map((key) => ({
key, key,
dqqy: currMeterHourly.has(key) ? Math.round(currMeterHourly.get(key)!) : null, dqqy: currExpenseDaily.has(key) ? Math.round(currExpenseDaily.get(key)! * 100) / 100 : null,
dbqy: selMeterHourly.has(key) ? Math.round(selMeterHourly.get(key)!) : null, dbqy: selExpenseDaily.has(key) ? Math.round(selExpenseDaily.get(key)! * 100) / 100 : null,
})); }));
// 环境对比:六项指标分别取两区域并集小时,缺数据侧输出 null(不补零) // 环境对比:六项指标分别取两区域并集小时,缺数据侧输出 null(不补零)
...@@ -1355,35 +1406,24 @@ export async function getAnalysisPopupSubTrend(currRegionKey: string, selectedRe ...@@ -1355,35 +1406,24 @@ export async function getAnalysisPopupSubTrend(currRegionKey: string, selectedRe
} }
/** /**
* 查询某区域电表设备在指定时间范围内的耗电量,按小时聚合(求和) * 查询某区域在指定日期范围内(expense_date,YYYY-MM-DD)的每日用电量(数据源:region_expenses 区域电费表)
* @returns Map<hourKey, 该小时耗电量总和> * @returns Map<dateKey, 该日用电量>
*/ */
async function queryRegionMeterHourly(regionKey: string, startTime: string, endTime: string): Promise<Map<string, number>> { async function queryRegionExpenseDaily(regionKey: string, startDate: string, endDate: string): Promise<Map<string, number>> {
const devices = await getDevicesByRegion(regionKey, ['device_id', 'device_type']); const dailyMap = new Map<string, number>();
const meterIds = devices.filter((d: any) => d.device_type === '电表').map((d: any) => d.device_id);
if (meterIds.length === 0) return new Map();
const dataResult = await selectDataListByParam( const dataResult = await selectDataListByParam(
TABLENAME.设备数据表, TABLENAME.区域电费表,
{ {
device_id: { '%in%': meterIds }, region_key: regionKey,
device_time: { '%gte%': startTime, '%lte%': endTime }, expense_date: { '%gte%': startDate, '%lte%': endDate },
'%orderAsc%': 'device_time', '%orderAsc%': 'expense_date',
}, },
['device_data', 'device_time'] ['expense_date', 'use_power']
); );
const hourlyMap = new Map<string, number>();
(dataResult.data || []).forEach((r: any) => { (dataResult.data || []).forEach((r: any) => {
const t = new Date(r.device_time); dailyMap.set(r.expense_date, Number(r.use_power) || 0);
const hourKey = `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')} ${String(t.getHours()).padStart(2, '0')}:00`;
const parsed = parseDeviceData(r.device_data);
const val = parsed && parsed[ELECTRICITY_KEY];
if (typeof val === 'number') {
hourlyMap.set(hourKey, (hourlyMap.get(hourKey) || 0) + val);
}
}); });
return hourlyMap; return dailyMap;
} }
/** /**
...@@ -2415,7 +2455,9 @@ export async function controlAcRunning(params: {}) { ...@@ -2415,7 +2455,9 @@ export async function controlAcRunning(params: {}) {
// "tempSetHi": Number(maxTemp), // "tempSetHi": Number(maxTemp),
"openApiAction": "control" "openApiAction": "control"
}; };
const result = await controlIndoorUnit(acPrarms); // 3. 调用飞奕控制接口
const result = { success:false,data:{},message:"" };
// result = await controlIndoorUnit(acParams);
// 手动控制日志 // 手动控制日志
const now = new Date(); const now = new Date();
const operationTime = new Date(); const operationTime = new Date();
......
...@@ -13,6 +13,7 @@ export enum TABLENAME { ...@@ -13,6 +13,7 @@ export enum TABLENAME {
日程设备关联表 = 'schedule_device', 日程设备关联表 = 'schedule_device',
设备故障表 = 'device_fault', 设备故障表 = 'device_fault',
设备联动调节表 = 'device_linkage', 设备联动调节表 = 'device_linkage',
设备联动状态表 = 'device_linkage_state',
设备扩展表 = 'device_expansion', 设备扩展表 = 'device_expansion',
区域电费表 = 'region_expenses', 区域电费表 = 'region_expenses',
}; };
......
...@@ -484,7 +484,7 @@ export const TablesConfig = [ ...@@ -484,7 +484,7 @@ export const TablesConfig = [
comment: '日志标题' comment: '日志标题'
}, },
operation_content: { operation_content: {
type: Sequelize.TEXT, type: Sequelize.JSON,
allowNull: true, allowNull: true,
comment: '操作内容' comment: '操作内容'
}, },
...@@ -659,6 +659,27 @@ export const TablesConfig = [ ...@@ -659,6 +659,27 @@ export const TablesConfig = [
allowNull: true, allowNull: true,
comment: '中间设置:处于阈值区间内的设定值' comment: '中间设置:处于阈值区间内的设定值'
}, },
fan_speed_max: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '上限风速:高风/中风/低风'
},
fan_speed_mid: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '中间风速:高风/中风/低风'
},
fan_speed_min: {
type: DataTypes.STRING(50),
allowNull: true,
comment: '下限风速:高风/中风/低风'
},
is_use: {
type: DataTypes.STRING(10),
allowNull: true,
defaultValue: 'on',
comment: '是否启用:on=启用, off=停用'
},
divide_method: { divide_method: {
type: DataTypes.STRING(50), type: DataTypes.STRING(50),
allowNull: true, allowNull: true,
...@@ -694,6 +715,67 @@ export const TablesConfig = [ ...@@ -694,6 +715,67 @@ export const TablesConfig = [
{ fields: ['device_id', 'linkage_type'] }, { fields: ['device_id', 'linkage_type'] },
] ]
}, },
// 设备联动状态表(记录每台空调上次联动下发的目标状态,用于下发去重与人为修改检测)
{
tableNameCn: '设备联动状态表',
tableName: 'device_linkage_state',
schema: {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
comment: '自增主键'
},
device_id: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
comment: '设备编号,关联device.device_id'
},
target_power: {
type: DataTypes.STRING(10),
allowNull: true,
comment: '目标开关机:开/关'
},
target_set_temp: {
type: DataTypes.STRING(20),
allowNull: true,
comment: '目标设定温度(新风联动为空)'
},
target_fan_speed: {
type: DataTypes.STRING(20),
allowNull: true,
comment: '目标风速:高风/中风/低风'
},
target_work_mode: {
type: DataTypes.STRING(20),
allowNull: true,
comment: '目标运行模式:制冷/制热/送风'
},
update_time: {
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'] },
]
},
// 区域电费表(按区域存储电费数据,不再关联设备) // 区域电费表(按区域存储电费数据,不再关联设备)
{ {
tableNameCn: '区域电费表', tableNameCn: '区域电费表',
......
...@@ -333,7 +333,7 @@ async function getDevicesByRegion(req, res) { ...@@ -333,7 +333,7 @@ async function getDevicesByRegion(req, res) {
*/ */
async function editDevice(req, res) { async function editDevice(req, res) {
let reqConf = { deviceId: 'String', power: 'String', setTemp: 'Number', fanSpeed: 'String', deviceMode: 'String' }; let reqConf = { deviceId: 'String', power: 'String', setTemp: 'Number', fanSpeed: 'String', deviceMode: 'String' };
const NotMustHaveKeys = ["fanSpeed", "deviceMode"]; const NotMustHaveKeys = ["setTemp", "fanSpeed", "deviceMode"];
let { deviceId, power, setTemp, fanSpeed, deviceMode } = eccReqParamater(reqConf, req.body, NotMustHaveKeys); let { deviceId, power, setTemp, fanSpeed, deviceMode } = eccReqParamater(reqConf, req.body, NotMustHaveKeys);
if (!deviceId || !power) { if (!deviceId || !power) {
res.success({success: false, msg: "缺少必填参数:deviceId, power"}); res.success({success: false, msg: "缺少必填参数:deviceId, power"});
......
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