vav.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. # -*- coding: utf-8 -*-
  2. from operator import attrgetter
  3. from typing import Dict, List, Optional, Tuple
  4. import numpy as np
  5. from fastapi import HTTPException
  6. from httpx import AsyncClient
  7. from loguru import logger
  8. from sqlalchemy.orm import Session
  9. from app.controllers.equipment.controller import EquipmentController
  10. from app.crud.space.weight import get_weights_by_vav
  11. from app.models.domain.devices import (
  12. ACATVAInstructionsRequest,
  13. ACATVAInstructionsRequestV2,
  14. )
  15. from app.schemas.equipment import VAVBox, FCU
  16. from app.schemas.instructions import ACATVAInstructions
  17. from app.schemas.sapce_weight import SpaceWeight
  18. from app.schemas.space import SpaceATVA
  19. from app.services.platform import DataPlatformService
  20. from app.services.transfer import Duoduo, SpaceInfoService, Season
  21. from app.utils.date import get_time_str
  22. class VAVController(EquipmentController):
  23. def __init__(self, equipment: VAVBox):
  24. super(VAVController, self).__init__()
  25. self.equipment = equipment
  26. async def get_strategy(self):
  27. strategy = "Plan A"
  28. for space in self.equipment.spaces:
  29. for eq in space.equipment:
  30. if isinstance(eq, FCU):
  31. strategy = "Plan B"
  32. break
  33. return strategy
  34. async def build_virtual_temperature(self) -> Tuple[float, float]:
  35. target_list, realtime_list = [], []
  36. buffer_list = []
  37. strategy = await self.get_strategy()
  38. for space in self.equipment.spaces:
  39. if not np.isnan(space.temperature_target):
  40. target_list.append(space.temperature_target)
  41. realtime_list.append(space.realtime_temperature)
  42. if strategy == "Plan B":
  43. for eq in space.equipment:
  44. if isinstance(eq, FCU):
  45. buffer = (4 - eq.air_valve_speed) / 4
  46. buffer_list.append(buffer)
  47. break
  48. total_target = buffer_list + target_list
  49. total_realtime = buffer_list + realtime_list
  50. if total_target and total_realtime:
  51. target_result = np.array(total_target).sum() / len(target_list)
  52. realtime_result = np.array(total_realtime).sum() / len(realtime_list)
  53. self.equipment.setting_temperature = target_result
  54. else:
  55. target_result, realtime_result = np.NAN, np.NAN
  56. return target_result, realtime_result
  57. async def get_supply_air_flow_set(
  58. self, temperature_set: float, temperature_realtime: float
  59. ) -> float:
  60. if np.isnan(temperature_set) or np.isnan(temperature_realtime):
  61. supply_air_flow_set = 0.0
  62. else:
  63. temperature_supply = self.equipment.supply_air_temperature
  64. if np.isnan(temperature_supply):
  65. temperature_supply = 19.0
  66. try:
  67. ratio = abs(
  68. 1
  69. + (temperature_realtime - temperature_set)
  70. / (temperature_set - temperature_supply)
  71. )
  72. except ZeroDivisionError:
  73. ratio = 1
  74. supply_air_flow_set = self.equipment.supply_air_flow * ratio
  75. supply_air_flow_set = max(
  76. self.equipment.supply_air_flow_lower_limit, supply_air_flow_set
  77. )
  78. supply_air_flow_set = min(
  79. self.equipment.supply_air_flow_upper_limit, supply_air_flow_set
  80. )
  81. self.equipment.supply_air_flow_set = supply_air_flow_set
  82. self.equipment.virtual_target_temperature = temperature_set
  83. self.equipment.virtual_realtime_temperature = temperature_realtime
  84. return supply_air_flow_set
  85. async def run(self):
  86. temperature_set, temperature_realtime = await self.build_virtual_temperature()
  87. await self.get_supply_air_flow_set(temperature_set, temperature_realtime)
  88. self.equipment.running_status = True
  89. def get_results(self):
  90. return self.equipment
  91. class VAVControllerV2(VAVController):
  92. def __init__(
  93. self,
  94. equipment: VAVBox,
  95. weights: Optional[List[SpaceWeight]] = None,
  96. season: Optional[Season] = None,
  97. ):
  98. super(VAVControllerV2, self).__init__(equipment)
  99. self.weights = weights
  100. self.season = season
  101. async def build_virtual_temperature(self) -> None:
  102. valid_spaces = []
  103. weights = []
  104. for sp in self.equipment.spaces:
  105. if sp.realtime_temperature > 0.0 and sp.temperature_target > 0.0:
  106. valid_spaces.append(sp)
  107. for weight in self.weights:
  108. if weight.space_id == sp.id:
  109. weights.append(weight)
  110. if valid_spaces:
  111. weights = sorted(weights, key=lambda x: x.temporary_weight_update_time)
  112. if weights[-1].temporary_weight_update_time > get_time_str(
  113. 60 * 60 * 2, flag="ago"
  114. ):
  115. # If someone has submitted a feedback in past two hours, meet the need.
  116. weight_dic = {weight.space_id: 0.0 for weight in weights}
  117. weight_dic.update({weights[-1].space_id: weights[-1].temporary_weight})
  118. else:
  119. weight_dic = {
  120. weight.space_id: weight.default_weight for weight in weights
  121. }
  122. total_weight_value = 0.0
  123. for v in weight_dic.values():
  124. total_weight_value += v
  125. if total_weight_value > 0:
  126. weight_dic = {
  127. k: v / total_weight_value for k, v in weight_dic.items()
  128. }
  129. else:
  130. weight_dic.update({list(weight_dic.keys())[0]: 1.0})
  131. try:
  132. virtual_target, virtual_realtime = 0.0, 0.0
  133. for sp in valid_spaces:
  134. virtual_target += sp.temperature_target * weight_dic.get(sp.id)
  135. virtual_realtime += sp.realtime_temperature * weight_dic.get(sp.id)
  136. except KeyError:
  137. logger.error(f"{self.equipment.id} has wrong vav-space relation")
  138. raise HTTPException(
  139. status_code=404, detail="This VAV box has wrong eq-sp relation"
  140. )
  141. self.equipment.virtual_target_temperature = virtual_target
  142. self.equipment.virtual_realtime_temperature = virtual_realtime
  143. else:
  144. self.equipment.virtual_target_temperature = np.NAN
  145. self.equipment.virtual_realtime_temperature = np.NAN
  146. async def rectify(self) -> Tuple[float, float]:
  147. bad_spaces = list()
  148. for sp in self.equipment.spaces:
  149. if self.season == Season.heating:
  150. if sp.realtime_temperature > max(
  151. 26.0, sp.temperature_target
  152. ) or sp.realtime_temperature < min(20.0, sp.temperature_target):
  153. if sp.temperature_target > 0.0:
  154. bad_spaces.append(sp)
  155. elif self.season == Season.cooling:
  156. if sp.realtime_temperature > max(
  157. 27.0, sp.temperature_target
  158. ) or sp.realtime_temperature < min(22.0, sp.temperature_target):
  159. if sp.temperature_target > 0.0:
  160. bad_spaces.append(sp)
  161. if bad_spaces:
  162. virtual_diff = self.equipment.virtual_target_temperature - self.equipment.virtual_realtime_temperature
  163. if self.season == Season.cooling:
  164. bad_spaces = sorted(bad_spaces, key=attrgetter("diff"))
  165. worst = bad_spaces[0]
  166. if worst.diff * virtual_diff >= 0:
  167. if abs(worst.diff) > abs(virtual_diff):
  168. self.equipment.virtual_target_temperature = worst.temperature_target
  169. self.equipment.virtual_realtime_temperature = worst.realtime_temperature
  170. else:
  171. if worst.diff < 0:
  172. self.equipment.virtual_target_temperature = min(22.0, worst.temperature_target) + 0.5
  173. else:
  174. self.equipment.virtual_target_temperature = max(26.0, worst.temperature_target) - 0.5
  175. self.equipment.virtual_realtime_temperature = worst.realtime_temperature
  176. elif self.season == Season.heating:
  177. bad_spaces = sorted(bad_spaces, key=attrgetter("diff"), reverse=True)
  178. worst = bad_spaces[0]
  179. if worst.diff * virtual_diff >= 0:
  180. if abs(worst.diff) > abs(virtual_diff):
  181. self.equipment.virtual_target_temperature = worst.temperature_target
  182. self.equipment.virtual_realtime_temperature = worst.realtime_temperature
  183. else:
  184. if worst.diff > 0:
  185. self.equipment.virtual_target_temperature = max(26.0, worst.temperature_target) - 0.5
  186. else:
  187. self.equipment.virtual_target_temperature = min(20.0, worst.temperature_target) + 0.5
  188. self.equipment.virtual_realtime_temperature = worst.realtime_temperature
  189. return (
  190. self.equipment.virtual_target_temperature,
  191. self.equipment.virtual_realtime_temperature,
  192. )
  193. async def run(self) -> None:
  194. await self.build_virtual_temperature()
  195. temperature_set, temperature_realtime = await self.rectify()
  196. await self.get_supply_air_flow_set(temperature_set, temperature_realtime)
  197. self.equipment.running_status = True
  198. class VAVControllerV3(VAVControllerV2):
  199. def __init__(self, vav_params: VAVBox, season: Season):
  200. super(VAVControllerV3, self).__init__(vav_params)
  201. self.season = season
  202. def get_valid_spaces(self) -> List:
  203. valid_spaces = list()
  204. for sp in self.equipment.spaces:
  205. if sp.realtime_temperature > 0.0 and sp.temperature_target > 0.0:
  206. valid_spaces.append(sp)
  207. return valid_spaces
  208. async def build_virtual_temperature(self) -> None:
  209. valid_spaces = self.get_valid_spaces()
  210. if not valid_spaces:
  211. virtual_realtime, virtual_target = np.NAN, np.NAN
  212. else:
  213. sorted_spaces = sorted(
  214. valid_spaces, key=lambda x: x.vav_temporary_update_time
  215. )
  216. if sorted_spaces[-1].vav_temporary_update_time > get_time_str(
  217. 60 * 60 * 2, flag="ago"
  218. ):
  219. virtual_realtime = sorted_spaces[-1].realtime_temperature
  220. virtual_target = sorted_spaces[-1].temperature_target
  221. else:
  222. virtual_realtime, virtual_target = 0.0, 0.0
  223. total_weight = 0.0
  224. for sp in valid_spaces:
  225. temp_weight = sp.vav_default_weight
  226. virtual_realtime += sp.realtime_temperature * temp_weight
  227. virtual_target += sp.temperature_target * temp_weight
  228. total_weight += temp_weight
  229. if total_weight == 0:
  230. for sp in valid_spaces:
  231. virtual_realtime += sp.realtime_temperature
  232. virtual_target += sp.temperature_target
  233. virtual_realtime /= len(valid_spaces)
  234. virtual_target /= len(valid_spaces)
  235. else:
  236. virtual_realtime /= total_weight
  237. virtual_target /= total_weight
  238. self.equipment.virtual_realtime_temperature = virtual_realtime
  239. self.equipment.virtual_target_temperature = virtual_target
  240. class VAVControllerV4(VAVControllerV3):
  241. def __init__(self, vav_params: VAVBox, season: Season, return_air_temp: float):
  242. super().__init__(vav_params, season)
  243. self.return_air_temp = return_air_temp
  244. def get_next_temp_set(
  245. self, virtual_realtime_temp: float, virtual_target_temp: float
  246. ) -> float:
  247. if np.isnan(virtual_realtime_temp) or np.isnan(virtual_target_temp):
  248. next_temp_set = np.NAN
  249. else:
  250. next_temp_set = (
  251. virtual_target_temp + self.return_air_temp - virtual_realtime_temp
  252. )
  253. self.equipment.setting_temperature = next_temp_set
  254. return next_temp_set
  255. async def run(self) -> None:
  256. await self.build_virtual_temperature()
  257. temperature_set, temperature_realtime = await self.rectify()
  258. self.get_next_temp_set(temperature_realtime, temperature_set)
  259. self.equipment.running_status = True
  260. async def fetch_vav_control_params(project_id: str, equipment_id: str) -> Dict:
  261. async with AsyncClient() as client:
  262. duo_duo = Duoduo(client, project_id)
  263. platform = DataPlatformService(client, project_id)
  264. season = await duo_duo.get_season()
  265. served_spaces = await duo_duo.get_space_by_equipment(equipment_id)
  266. space_objects = []
  267. realtime_supply_air_temperature_list = []
  268. for sp in served_spaces:
  269. sp_id = sp.get("id")
  270. transfer = SpaceInfoService(client, project_id, sp_id)
  271. current_target = await transfer.get_current_temperature_target()
  272. realtime_temperature = await platform.get_realtime_temperature(sp_id)
  273. related_equipment = await transfer.get_equipment()
  274. equipment_objects = []
  275. for eq in related_equipment:
  276. if eq.get("category") == "ACATFC":
  277. speed = await platform.get_fan_speed(eq.get("id"))
  278. temp_fcu_params = {"id": eq.get("id"), "air_valve_speed": speed}
  279. fcu = FCU(**temp_fcu_params)
  280. equipment_objects.append(fcu)
  281. elif eq.get("category") == "ACATAH":
  282. realtime_supply_air_temperature_list.append(
  283. await platform.get_realtime_supply_air_temperature(eq.get("id"))
  284. )
  285. temp_space_params = {
  286. "id": sp_id,
  287. "realtime_temperature": realtime_temperature,
  288. "equipment": equipment_objects,
  289. "temperature_target": current_target,
  290. "diff": current_target - realtime_temperature,
  291. }
  292. space = SpaceATVA(**temp_space_params)
  293. space_objects.append(space)
  294. realtime_supply_air_temperature = np.array(
  295. realtime_supply_air_temperature_list
  296. ).mean()
  297. realtime_supply_air_flow = await platform.get_realtime_supply_air_flow(
  298. equipment_id
  299. )
  300. lower_limit, upper_limit = await platform.get_air_flow_limit(equipment_id)
  301. vav_params = {
  302. "id": equipment_id,
  303. "spaces": space_objects,
  304. "supply_air_temperature": realtime_supply_air_temperature,
  305. "supply_air_flow": realtime_supply_air_flow,
  306. "supply_air_flow_lower_limit": lower_limit,
  307. "supply_air_flow_upper_limit": upper_limit,
  308. "season": season,
  309. }
  310. return vav_params
  311. @logger.catch()
  312. async def get_vav_control_v1(project: str, equipment_id: str) -> VAVBox:
  313. vav_params = await fetch_vav_control_params(project, equipment_id)
  314. vav = VAVBox(**vav_params)
  315. vav_controller = VAVController(vav)
  316. await vav_controller.run()
  317. regulated_vav = vav_controller.get_results()
  318. return regulated_vav
  319. @logger.catch()
  320. async def get_vav_control_v2(db: Session, project_id: str, equipment_id: str) -> VAVBox:
  321. vav_params = await fetch_vav_control_params(project_id, equipment_id)
  322. vav = VAVBox(**vav_params)
  323. weights = get_weights_by_vav(db, equipment_id)
  324. vav_controller = VAVControllerV2(
  325. vav, [SpaceWeight.from_orm(weight) for weight in weights], vav_params["season"]
  326. )
  327. await vav_controller.run()
  328. regulated_vav = vav_controller.get_results()
  329. return regulated_vav
  330. @logger.catch()
  331. async def build_acatva_instructions(
  332. params: ACATVAInstructionsRequest,
  333. ) -> ACATVAInstructions:
  334. space_params = []
  335. for sp in params.spaces:
  336. temp_sp = SpaceATVA(**sp.dict())
  337. temp_sp.diff = temp_sp.temperature_target - temp_sp.realtime_temperature
  338. space_params.append(temp_sp)
  339. if params.supply_air_temperature == -1:
  340. if params.acatah_supply_air_temperature == -1:
  341. supply_air_temperature = np.NAN
  342. else:
  343. supply_air_temperature = params.acatah_supply_air_temperature
  344. else:
  345. supply_air_temperature = params.supply_air_temperature
  346. vav_params = VAVBox(
  347. spaces=space_params,
  348. supply_air_temperature=supply_air_temperature,
  349. supply_air_flow=params.supply_air_flow,
  350. supply_air_flow_lower_limit=params.supply_air_flow_lower_limit,
  351. supply_air_flow_upper_limit=params.supply_air_flow_upper_limit,
  352. )
  353. controller = VAVControllerV3(vav_params=vav_params, season=Season(params.season))
  354. await controller.run()
  355. regulated_vav = controller.get_results()
  356. instructions = ACATVAInstructions(
  357. supply_air_flow_set=regulated_vav.supply_air_flow_set,
  358. virtual_realtime_temperature=regulated_vav.virtual_realtime_temperature,
  359. virtual_temperature_target_set=regulated_vav.virtual_target_temperature,
  360. )
  361. return instructions
  362. @logger.catch()
  363. async def build_acatva_instructions_for_JM(
  364. params: ACATVAInstructionsRequestV2,
  365. ) -> dict:
  366. # Control logic for Jiaming.
  367. space_params = []
  368. for sp in params.spaces:
  369. temp_sp = SpaceATVA(**sp.dict())
  370. if temp_sp.temperature_target:
  371. temp_sp.diff = temp_sp.temperature_target - temp_sp.realtime_temperature
  372. else:
  373. temp_sp.diff = 0.0
  374. space_params.append(temp_sp)
  375. vav_params = VAVBox(spaces=space_params)
  376. return_air_temp = (
  377. np.NAN if params.return_air_temp == -1.0 else params.return_air_temp
  378. )
  379. controller = VAVControllerV4(
  380. vav_params=vav_params,
  381. season=Season(params.season),
  382. return_air_temp=return_air_temp,
  383. )
  384. await controller.run()
  385. regulated_vav = controller.get_results()
  386. next_temp_set = regulated_vav.setting_temperature
  387. if next_temp_set:
  388. if np.isnan(next_temp_set):
  389. next_temp_set = -1.0
  390. else:
  391. next_temp_set = -1.0
  392. instructions = {
  393. 'temperature_target_set': next_temp_set,
  394. 'virtual_target_temperature': regulated_vav.virtual_target_temperature,
  395. 'virtual_realtime_temperature': regulated_vav.virtual_realtime_temperature
  396. }
  397. return instructions