vav.py 16 KB

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