vav.py 15 KB

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