Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
D
dstTechnologyPlatform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
node_server
dstTechnologyPlatform
Commits
1be0dee2
Commit
1be0dee2
authored
Sep 03, 2026
by
PC-20251223ZVQQ\Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
空调联动管理
parent
a716b29d
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
688 additions
and
101 deletions
+688
-101
环境品质指标计算公式.xlsx
res/环境品质指标计算公式.xlsx
+0
-0
acLinkage.ts
src/biz/acLinkage.ts
+315
-0
customs.ts
src/biz/customs.ts
+10
-10
customsIndicators.ts
src/biz/customsIndicators.ts
+5
-5
dataIntegration.ts
src/biz/dataIntegration.ts
+3
-0
device.ts
src/biz/device.ts
+7
-6
region.ts
src/biz/region.ts
+160
-17
running.ts
src/biz/running.ts
+103
-61
dbEnum.ts
src/config/dbEnum.ts
+1
-0
mysqlTableConfig.ts
src/config/mysqlTableConfig.ts
+83
-1
admin.ts
src/routers/admin.ts
+1
-1
No files found.
res/环境品质指标计算公式.xlsx
0 → 100644
View file @
1be0dee2
File added
src/biz/acLinkage.ts
0 → 100644
View file @
1be0dee2
/**
* 环境联动控制:迪勤环境数据同步后,根据 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
{};
}
}
src/biz/customs.ts
View file @
1be0dee2
...
...
@@ -11,11 +11,11 @@
* // 热湿环境
* 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.28
ppm", 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.28
lux
", ugr: "5.6%", ghjTrend: [...] }
* // 声环境
* shj: { dB: "28
", nr: "NR-222", maxdB: "56
", shjTrend: [...] }
* shj: { dB: "28
dB", nr: "NR-222", maxdB: "56dB
", shjTrend: [...] }
* // 区域综合舒适度
* 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<{
for
(
const
row
of
latestData
)
{
const
rk
=
deviceRegionMap
[
row
.
device_id
];
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
;
if
(
d
.
temperature
!==
undefined
)
regionAgg
[
rk
].
temp
.
push
(
d
.
temperature
);
if
(
d
.
humidity
!==
undefined
)
regionAgg
[
rk
].
hum
.
push
(
d
.
humidity
);
...
...
@@ -529,10 +529,10 @@ export async function getCustomsAnalysis() {
pmvTrend
:
pmvTrendSliced
,
},
kqpz
:
{
co2
:
sensorData
.
co2
.
toFixed
(
2
),
pm25
:
sensorData
.
pm25
.
toFixed
(
2
),
tvoc
:
sensorData
.
tvoc
.
toFixed
(
2
),
pm10
:
(
sensorData
.
pm10
||
0
).
toFixed
(
2
),
co2
:
sensorData
.
co2
.
toFixed
(
2
)
+
'ppm'
,
pm25
:
sensorData
.
pm25
.
toFixed
(
2
)
+
'μg/m³'
,
tvoc
:
sensorData
.
tvoc
.
toFixed
(
2
)
+
'μg/m³'
,
pm10
:
(
sensorData
.
pm10
||
0
).
toFixed
(
2
)
+
'μg/m³'
,
kqpzTrend
:
airTrendSliced
,
},
ghj
:
{
...
...
@@ -541,9 +541,9 @@ export async function getCustomsAnalysis() {
ghjTrend
:
lightTrendSliced
,
},
shj
:
{
dB
:
sensorData
.
a14263
.
toFixed
(
2
),
dB
:
sensorData
.
a14263
.
toFixed
(
2
)
+
'dB'
,
nr
:
'NR-'
+
Math
.
round
(
nr
),
maxdB
:
maxNoise
.
toFixed
(
2
),
maxdB
:
maxNoise
.
toFixed
(
2
)
+
'dB'
,
shjTrend
:
noiseTrendSliced
,
},
qyzhssd
:
regionData
,
...
...
src/biz/customsIndicators.ts
View file @
1be0dee2
...
...
@@ -20,17 +20,17 @@ export const STANDARDS = {
};
/** PMV 舒适区间 [-0.5, +0.5] */
const
PMV_COMFORT_MIN
=
-
0.5
;
const
PMV_COMFORT_MAX
=
0.5
;
export
const
PMV_COMFORT_MIN
=
-
0.5
;
export
const
PMV_COMFORT_MAX
=
0.5
;
/** PPD 限值 ≤ 20% */
const
PPD_LIMIT
=
20
;
export
const
PPD_LIMIT
=
20
;
/** UGR 展厅限值 ≤ 22 */
const
UGR_LIMIT
=
22
;
export
const
UGR_LIMIT
=
22
;
/** NR 办公场所限值 ≤ 40 */
const
NR_LIMIT
=
40
;
export
const
NR_LIMIT
=
40
;
/** 噪音 24h 峰值告警阈值 > 65 dB(A) */
export
const
NOISE_ALERT_THRESHOLD
=
65
;
...
...
src/biz/dataIntegration.ts
View file @
1be0dee2
...
...
@@ -16,6 +16,7 @@ import {
getDataSourceDetail
,
}
from
"./diqinClient"
;
import
{
mysqlModelMap
}
from
"../model/sqlModelBind"
;
import
{
acLinkageControl
}
from
"./acLinkage"
;
import
{
systemConfig
}
from
"../config/serverConfig"
;
import
Sequelize
from
"sequelize"
;
import
{
DEVICE_STATE
,
WORK_MODE_TEXT_MAP
,
FAN_SPEED_TEXT_MAP
}
from
"../config/businessEnum"
;
...
...
@@ -1645,6 +1646,8 @@ export async function diqinSyncAll() {
await
diqinRegionDeviceRel
();
await
diqinDeviceRegionKey
();
await
diqinDeviceData
();
// 环境数据同步后触发空调自动联动控制
await
acLinkageControl
();
console
.
log
(
'[迪勤全量同步] 完成'
);
}
...
...
src/biz/device.ts
View file @
1be0dee2
...
...
@@ -283,7 +283,8 @@ export async function editAcDeviceControl(params: {
if
(
deviceMode
)
acParams
.
workMode
=
workModeMap
[
deviceMode
];
// 3. 调用飞奕控制接口
const
result
=
await
controlIndoorUnit
(
acParams
);
const
result
=
{
success
:
false
,
data
:{}
};
// result = await controlIndoorUnit(acParams);
// 4. 记录操作日志
const
now
=
new
Date
();
...
...
@@ -296,9 +297,9 @@ export async function editAcDeviceControl(params: {
device_id
:
deviceId
,
time
:
formatLocalTime
(
now
),
power
:
power
,
set
Temp
:
setTemp
,
fan
Speed
:
fanSpeed
,
device
Mode
:
deviceMode
,
set
_temp
:
setTemp
??
''
,
fan
_speed
:
fanSpeed
??
''
,
device
_mode
:
deviceMode
??
''
,
success
:
result
.
success
,
message
:
result
.
data
||
(
result
.
success
?
'控制成功'
:
'控制失败'
),
},
...
...
@@ -622,7 +623,7 @@ export async function batchEditLocalDevices(params: {
deviceName
?:
string
;
regionKey
?:
number
;
deviceType
?:
string
;
controlParams
?:
string
;
linkageStart
?:
string
;
deviceState
?:
string
;
};
})
{
...
...
@@ -645,7 +646,7 @@ export async function batchEditLocalDevices(params: {
if
(
edits
.
deviceName
!==
undefined
)
dbEdits
.
device_name
=
edits
.
deviceName
;
if
(
edits
.
regionKey
!==
undefined
)
dbEdits
.
region_key
=
edits
.
regionKey
;
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
(
Object
.
keys
(
dbEdits
).
length
===
0
)
{
...
...
src/biz/region.ts
View file @
1be0dee2
...
...
@@ -15,6 +15,17 @@ import { delData } from '../data/delData';
import
{
mysqlModelMap
}
from
'../model/sqlModelBind'
;
import
{
TABLENAME
}
from
'../config/dbEnum'
;
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'
);
...
...
@@ -142,6 +153,119 @@ function judgeFourIndicators(
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 映射,从数据库查询各区域下的设备及最新功率状态
* 同时提取最新 co2/pm25 数据计算各区域环境质量
...
...
@@ -155,10 +279,12 @@ async function buildDeviceMapDynamic(
):
Promise
<
{
deviceMap
:
{
[
regionName
:
string
]:
any
[]
};
qualityMap
:
{
[
regionName
:
string
]:
string
};
targetsMap
:
{
[
regionName
:
string
]:
RegionTargetItem
[]
};
dataMap
:
{
[
deviceId
:
string
]:
any
};
}
>
{
const
deviceMap
:
{
[
regionName
:
string
]:
any
[]
}
=
{};
const
qualityMap
:
{
[
regionName
:
string
]:
string
}
=
{};
const
targetsMap
:
{
[
regionName
:
string
]:
RegionTargetItem
[]
}
=
{};
const
dataMap
:
{
[
deviceId
:
string
]:
any
}
=
{};
// 0. 根据 sheet 解析出的设备编号(已剔除占位符)查询设备表,构建 device_id → 设备信息映射
...
...
@@ -175,7 +301,7 @@ async function buildDeviceMapDynamic(
// 1. 收集所有 region_key
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
const
regionKeyToName
:
{
[
key
:
string
]:
string
}
=
{};
...
...
@@ -195,10 +321,10 @@ async function buildDeviceMapDynamic(
const
deviceIds
=
devices
.
map
((
d
)
=>
d
.
device_id
).
filter
(
Boolean
);
const
powerMap
=
new
Map
<
string
,
string
>
();
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
))
{
regionEnvMap
[
name
]
=
{
co2List
:
[],
pm25List
:
[]
};
regionEnvMap
[
name
]
=
{};
}
if
(
deviceIds
.
length
>
0
)
{
// 计算时间截断点:当前时间减去 hour 小时(不截断整点)
...
...
@@ -237,12 +363,17 @@ async function buildDeviceMapDynamic(
powerMap
.
set
(
r
.
device_id
,
parsed
?.
power
||
'on'
);
deviceDataMap
.
set
(
r
.
device_id
,
parsed
);
// 提取环境数据
// 提取环境数据
(仅收集区域内有效数值型指标)
const
rk
=
devices
.
find
(
d
=>
d
.
device_id
===
r
.
device_id
)?.
region_key
;
const
rn
=
rk
?
regionKeyToName
[
rk
]
:
undefined
;
if
(
rn
&&
regionEnvMap
[
rn
])
{
if
(
typeof
parsed
?.
co2
===
'number'
)
regionEnvMap
[
rn
].
co2List
.
push
(
parsed
.
co2
);
if
(
typeof
parsed
?.
pm25
===
'number'
)
regionEnvMap
[
rn
].
pm25List
.
push
(
parsed
.
pm25
);
for
(
const
metric
of
ENV_METRICS
)
{
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(
}
}
// 6. 根据各区域环境数据计算空气质量等级
for
(
const
[
name
,
entry
]
of
Object
.
entries
(
regionEnvMap
))
{
const
avgCo2
=
entry
.
co2List
.
length
>
0
?
entry
.
co2List
.
reduce
((
a
,
b
)
=>
a
+
b
,
0
)
/
entry
.
co2List
.
length
:
0
;
const
avgPm25
=
entry
.
pm25List
.
length
>
0
?
entry
.
pm25List
.
reduce
((
a
,
b
)
=>
a
+
b
,
0
)
/
entry
.
pm25List
.
length
:
0
;
qualityMap
[
name
]
=
judgeQuality
(
avgCo2
,
avgPm25
,
entry
.
co2List
.
length
>
0
,
entry
.
pm25List
.
length
>
0
);
// 6. 根据各区域环境数据计算空气质量等级 + 四维度 targets
for
(
const
name
of
Object
.
keys
(
regionMap
))
{
const
entry
=
regionEnvMap
[
name
];
// 计算各指标均值(无数据时为 undefined)
const
avgOf
=
(
metric
:
EnvMetric
):
number
|
undefined
=>
{
const
list
=
entry
[
metric
];
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 ============================================
...
...
@@ -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
result
=
[];
if
(
sheetData
&&
sheetData
[
0
].
blockData
)
{
...
...
@@ -419,7 +562,7 @@ async function readSheet(sheet: any[][], gatewayPage: number, hour: number) {
deviceId
:
row
[
dindex
],
power
:
row
[
dindex
]
?
(
dataMap
[
row
[
dindex
]]?.
device_state
&&
dataMap
[
row
[
dindex
]]?.
device_state
===
'1'
?
'off'
:
'on'
)
:
undefined
,
devices
:
deviceMap
[
row
[
0
]],
targets
:
[]
targets
:
targetsMap
[
row
[
0
]]
??
[]
}))??
[];
}
return
result
;
...
...
src/biz/running.ts
View file @
1be0dee2
...
...
@@ -283,6 +283,9 @@ function parseDeviceData(raw: any): any {
// TODO: 电表数据接入后,确认 device_data JSON 中电量字段的 key,目前暂定 'current'
const
ELECTRICITY_KEY
=
'current'
;
/** 区域用电逐日趋势展示最近 N 天(region_expenses 区域电费表按天聚合) */
const
ELECTRICITY_TREND_DAYS
=
14
;
/**
* 查询电表设备在指定时间范围内的电量总和
*/
...
...
@@ -1232,47 +1235,91 @@ export async function getAnalysisPopup(regionKey: string) {
}
sbztjcqs
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
// 能耗监测(该区域
电表24小时趋势
)
// 能耗监测(该区域
用电量趋势,数据源:region_expenses 区域电费表,按天一条
)
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
);
if
(
meterIds
.
length
>
0
)
{
const
meterDataResult
=
await
selectDataListByParam
(
{
const
endDate
=
formatTime
(
new
Date
()).
split
(
' '
)[
0
];
const
startDt
=
new
Date
();
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
.
设备数据表
,
{
device_id
:
{
'%in%'
:
meter
Ids
},
device_id
:
{
'%in%'
:
ieq
Ids
},
device_time
:
{
'%gte%'
:
trendStart
,
'%lte%'
:
todayEnd
},
'%orderAsc%'
:
'device_time'
,
},
[
'device_data'
,
'device_time'
]
);
const
hourlySum
=
new
Map
<
string
,
number
>
();
(
meterDataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
// 按小时取最新一条:device_time 升序遍历,后写覆盖
const
hourlyMap
=
new
Map
<
string
,
Map
<
string
,
number
>>
();
(
envDataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
const
t
=
new
Date
(
r
.
device_time
);
const
h
=
String
(
t
.
getHours
()).
padStart
(
2
,
'0'
);
const
hourKey
=
todayStart
.
split
(
' '
)[
0
]
+
' '
+
h
+
':00'
;
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
[
ELECTRICITY_KEY
];
if
(
typeof
val
===
'number'
)
{
hourlySum
.
set
(
hourKey
,
(
hourlySum
.
get
(
hourKey
)
||
0
)
+
val
);
}
if
(
!
parsed
)
return
;
if
(
!
hourlyMap
.
has
(
hourKey
))
hourlyMap
.
set
(
hourKey
,
new
Map
());
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
))
}))
.
sort
((
a
,
b
)
=>
a
.
key
.
localeCompare
(
b
.
key
));
// 组装六项输出(缺数据的指标输出空数组)
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
));
});
}
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
{
"regionName"
:
regionName
,
"sbztjc"
:
sbztjc
,
...
...
@@ -1307,26 +1354,30 @@ export async function getAnalysisPopupSubTrend(currRegionKey: string, selectedRe
====== MOCK DATA END ====== */
// ---- 真实查询 ----
//
时间范围:过去 24 小时 ~ 当前时刻
(只输出到当前小时,不查未来)
//
能耗:过去 N 天(日期粒度);环境:过去 24 小时
(只输出到当前小时,不查未来)
const
trendStart
=
getHoursAgo
(
24
);
const
trendEnd
=
formatTime
(
new
Date
());
// 两区域分别聚合:能耗(电表按小时求和)、环境(IEQ 按小时取最新一条)
const
[
currMeterHourly
,
selMeterHourly
]
=
await
Promise
.
all
([
queryRegionMeterHourly
(
currRegionKey
,
trendStart
,
trendEnd
),
queryRegionMeterHourly
(
selectedRegionKey
,
trendStart
,
trendEnd
),
const
expenseEndDate
=
formatTime
(
new
Date
()).
split
(
' '
)[
0
];
const
expenseStartDt
=
new
Date
();
expenseStartDt
.
setDate
(
expenseStartDt
.
getDate
()
-
(
ELECTRICITY_TREND_DAYS
-
1
));
const
expenseStartDate
=
`
${
expenseStartDt
.
getFullYear
()}
-
${
String
(
expenseStartDt
.
getMonth
()
+
1
).
padStart
(
2
,
'0'
)}
-
${
String
(
expenseStartDt
.
getDate
()).
padStart
(
2
,
'0'
)}
`
;
// 两区域分别聚合:能耗(region_expenses 区域电费表按天)、环境(IEQ 按小时取最新一条)
const
[
currExpenseDaily
,
selExpenseDaily
]
=
await
Promise
.
all
([
queryRegionExpenseDaily
(
currRegionKey
,
expenseStartDate
,
expenseEndDate
),
queryRegionExpenseDaily
(
selectedRegionKey
,
expenseStartDate
,
expenseEndDate
),
]);
const
[
currIeqHourly
,
selIeqHourly
]
=
await
Promise
.
all
([
queryRegionIeqHourly
(
currRegionKey
,
trendStart
,
trendEnd
),
queryRegionIeqHourly
(
selectedRegionKey
,
trendStart
,
trendEnd
),
]);
// 能耗对比:
hourK
ey 取两区域并集,缺数据侧输出 null(不补零)
const
nhjc
HourKeys
=
Array
.
from
(
new
Set
([...
currMeterHourly
.
keys
(),
...
selMeterHour
ly
.
keys
()])).
sort
();
let
nhjcdb
:
{
key
:
string
;
dqqy
:
number
|
null
,
dbqy
:
number
|
null
}[]
=
nhjc
Hour
Keys
.
map
((
key
)
=>
({
// 能耗对比:
日期 k
ey 取两区域并集,缺数据侧输出 null(不补零)
const
nhjc
DateKeys
=
Array
.
from
(
new
Set
([...
currExpenseDaily
.
keys
(),
...
selExpenseDai
ly
.
keys
()])).
sort
();
let
nhjcdb
:
{
key
:
string
;
dqqy
:
number
|
null
,
dbqy
:
number
|
null
}[]
=
nhjc
Date
Keys
.
map
((
key
)
=>
({
key
,
dqqy
:
curr
MeterHourly
.
has
(
key
)
?
Math
.
round
(
currMeterHourly
.
get
(
key
)
!
)
:
null
,
dbqy
:
sel
MeterHourly
.
has
(
key
)
?
Math
.
round
(
selMeterHourly
.
get
(
key
)
!
)
:
null
,
dqqy
:
curr
ExpenseDaily
.
has
(
key
)
?
Math
.
round
(
currExpenseDaily
.
get
(
key
)
!
*
100
)
/
100
:
null
,
dbqy
:
sel
ExpenseDaily
.
has
(
key
)
?
Math
.
round
(
selExpenseDaily
.
get
(
key
)
!
*
100
)
/
100
:
null
,
}));
// 环境对比:六项指标分别取两区域并集小时,缺数据侧输出 null(不补零)
...
...
@@ -1355,35 +1406,24 @@ export async function getAnalysisPopupSubTrend(currRegionKey: string, selectedRe
}
/**
* 查询某区域
电表设备在指定时间范围内的耗电量,按小时聚合(求和
)
* @returns Map<
hourKey, 该小时耗电量总和
>
* 查询某区域
在指定日期范围内(expense_date,YYYY-MM-DD)的每日用电量(数据源:region_expenses 区域电费表
)
* @returns Map<
dateKey, 该日用电量
>
*/
async
function
queryRegionMeterHourly
(
regionKey
:
string
,
startTime
:
string
,
endTime
:
string
):
Promise
<
Map
<
string
,
number
>>
{
const
devices
=
await
getDevicesByRegion
(
regionKey
,
[
'device_id'
,
'device_type'
]);
const
meterIds
=
devices
.
filter
((
d
:
any
)
=>
d
.
device_type
===
'电表'
).
map
((
d
:
any
)
=>
d
.
device_id
);
if
(
meterIds
.
length
===
0
)
return
new
Map
();
async
function
queryRegionExpenseDaily
(
regionKey
:
string
,
startDate
:
string
,
endDate
:
string
):
Promise
<
Map
<
string
,
number
>>
{
const
dailyMap
=
new
Map
<
string
,
number
>
();
const
dataResult
=
await
selectDataListByParam
(
TABLENAME
.
设备数据
表
,
TABLENAME
.
区域电费
表
,
{
device_id
:
{
'%in%'
:
meterIds
}
,
device_time
:
{
'%gte%'
:
startTime
,
'%lte%'
:
endTim
e
},
'%orderAsc%'
:
'
device_tim
e'
,
region_key
:
regionKey
,
expense_date
:
{
'%gte%'
:
startDate
,
'%lte%'
:
endDat
e
},
'%orderAsc%'
:
'
expense_dat
e'
,
},
[
'
device_data'
,
'device_time
'
]
[
'
expense_date'
,
'use_power
'
]
);
const
hourlyMap
=
new
Map
<
string
,
number
>
();
(
dataResult
.
data
||
[]).
forEach
((
r
:
any
)
=>
{
const
t
=
new
Date
(
r
.
device_time
);
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
);
}
dailyMap
.
set
(
r
.
expense_date
,
Number
(
r
.
use_power
)
||
0
);
});
return
hour
lyMap
;
return
dai
lyMap
;
}
/**
...
...
@@ -2415,7 +2455,9 @@ export async function controlAcRunning(params: {}) {
// "tempSetHi": Number(maxTemp),
"openApiAction"
:
"control"
};
const
result
=
await
controlIndoorUnit
(
acPrarms
);
// 3. 调用飞奕控制接口
const
result
=
{
success
:
false
,
data
:{},
message
:
""
};
// result = await controlIndoorUnit(acParams);
// 手动控制日志
const
now
=
new
Date
();
const
operationTime
=
new
Date
();
...
...
src/config/dbEnum.ts
View file @
1be0dee2
...
...
@@ -13,6 +13,7 @@ export enum TABLENAME {
日程设备关联表
=
'schedule_device'
,
设备故障表
=
'device_fault'
,
设备联动调节表
=
'device_linkage'
,
设备联动状态表
=
'device_linkage_state'
,
设备扩展表
=
'device_expansion'
,
区域电费表
=
'region_expenses'
,
};
...
...
src/config/mysqlTableConfig.ts
View file @
1be0dee2
...
...
@@ -484,7 +484,7 @@ export const TablesConfig = [
comment
:
'日志标题'
},
operation_content
:
{
type
:
Sequelize
.
TEXT
,
type
:
Sequelize
.
JSON
,
allowNull
:
true
,
comment
:
'操作内容'
},
...
...
@@ -659,6 +659,27 @@ export const TablesConfig = [
allowNull
:
true
,
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
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
true
,
...
...
@@ -694,6 +715,67 @@ export const TablesConfig = [
{
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
:
'区域电费表'
,
...
...
src/routers/admin.ts
View file @
1be0dee2
...
...
@@ -333,7 +333,7 @@ async function getDevicesByRegion(req, res) {
*/
async
function
editDevice
(
req
,
res
)
{
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
);
if
(
!
deviceId
||
!
power
)
{
res
.
success
({
success
:
false
,
msg
:
"缺少必填参数:deviceId, power"
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment