vav.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. # -*- coding: utf-8 -*-
  2. from operator import attrgetter
  3. import numpy as np
  4. from fastapi import HTTPException
  5. from loguru import logger
  6. from app.api.errors.iot import MissingIOTDataError
  7. from app.controllers.equipment.controller import EquipmentController
  8. from app.models.domain.devices import ACATVAInstructionsRequest, ACATVAInstructionsRequestV2
  9. from app.schemas.equipment import VAVBox, FCU
  10. from app.schemas.instructions import ACATVAInstructions
  11. from app.schemas.sapce_weight import SpaceWeight
  12. from app.schemas.space import SpaceATVA
  13. from app.services.transfer import Season
  14. from app.utils.date import get_time_str
  15. class VAVController(EquipmentController):
  16. def __init__(self, equipment: VAVBox):
  17. super(VAVController, self).__init__()
  18. self.equipment = equipment
  19. async def get_strategy(self):
  20. strategy = "Plan A"
  21. for space in self.equipment.spaces:
  22. for eq in space.equipment:
  23. if isinstance(eq, FCU):
  24. strategy = "Plan B"
  25. break
  26. return strategy
  27. async def build_virtual_temperature(self) -> tuple[float, float]:
  28. target_list, realtime_list = [], []
  29. buffer_list = []
  30. strategy = await self.get_strategy()
  31. for space in self.equipment.spaces:
  32. if not np.isnan(space.temperature_target):
  33. target_list.append(space.temperature_target)
  34. realtime_list.append(space.realtime_temperature)
  35. if strategy == "Plan B":
  36. for eq in space.equipment:
  37. if isinstance(eq, FCU):
  38. buffer = (4 - eq.air_valve_speed) / 4
  39. buffer_list.append(buffer)
  40. break
  41. total_target = buffer_list + target_list
  42. total_realtime = buffer_list + realtime_list
  43. if total_target and total_realtime:
  44. target_result = np.array(total_target).sum() / len(target_list)
  45. realtime_result = np.array(total_realtime).sum() / len(realtime_list)
  46. self.equipment.setting_temperature = target_result
  47. else:
  48. target_result, realtime_result = np.NAN, np.NAN
  49. return target_result, realtime_result
  50. async def get_supply_air_flow_set(
  51. self, temperature_set: float, temperature_realtime: float
  52. ) -> float:
  53. if not (temperature_set and temperature_realtime):
  54. supply_air_flow_set = 0.0
  55. else:
  56. temperature_supply = self.equipment.supply_air_temperature
  57. if np.isnan(temperature_supply):
  58. temperature_supply = 19.0
  59. try:
  60. ratio = abs(
  61. 1
  62. + (temperature_realtime - temperature_set)
  63. / (temperature_set - temperature_supply)
  64. )
  65. except ZeroDivisionError:
  66. ratio = 1
  67. supply_air_flow_set = self.equipment.supply_air_flow * ratio
  68. supply_air_flow_set = max(
  69. self.equipment.supply_air_flow_lower_limit, supply_air_flow_set
  70. )
  71. supply_air_flow_set = min(
  72. self.equipment.supply_air_flow_upper_limit, supply_air_flow_set
  73. )
  74. self.equipment.supply_air_flow_set = supply_air_flow_set
  75. self.equipment.virtual_target_temperature = temperature_set
  76. self.equipment.virtual_realtime_temperature = temperature_realtime
  77. return supply_air_flow_set
  78. async def run(self):
  79. temperature_set, temperature_realtime = await self.build_virtual_temperature()
  80. await self.get_supply_air_flow_set(temperature_set, temperature_realtime)
  81. self.equipment.running_status = True
  82. def get_results(self):
  83. return self.equipment
  84. class VAVControllerV2(VAVController):
  85. def __init__(
  86. self,
  87. equipment: VAVBox,
  88. weights: list[SpaceWeight] | None = None,
  89. season: Season | None = None,
  90. ):
  91. super(VAVControllerV2, self).__init__(equipment)
  92. self.weights = weights
  93. self.season = season
  94. def get_valid_spaces(self) -> list[SpaceATVA]:
  95. valid_spaces = list()
  96. for sp in self.equipment.spaces:
  97. if sp.realtime_temperature and sp.temperature_target:
  98. sp.diff = sp.temperature_target - sp.realtime_temperature
  99. valid_spaces.append(sp)
  100. return valid_spaces
  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. valid_spaces = self.get_valid_spaces()
  149. for sp in valid_spaces:
  150. if self.season == Season.heating:
  151. if sp.realtime_temperature > max(
  152. 26.0, sp.temperature_target
  153. ) or sp.realtime_temperature < min(20.0, sp.temperature_target):
  154. if sp.temperature_target > 0.0:
  155. bad_spaces.append(sp)
  156. elif self.season == Season.cooling:
  157. if sp.realtime_temperature > max(
  158. 27.0, sp.temperature_target
  159. ) or sp.realtime_temperature < min(22.0, sp.temperature_target):
  160. if sp.temperature_target > 0.0:
  161. bad_spaces.append(sp)
  162. if bad_spaces:
  163. virtual_diff = self.equipment.virtual_target_temperature - self.equipment.virtual_realtime_temperature
  164. if self.season == Season.cooling:
  165. bad_spaces = sorted(bad_spaces, key=attrgetter("diff"))
  166. worst = bad_spaces[0]
  167. if worst.diff * virtual_diff >= 0:
  168. if abs(worst.diff) > abs(virtual_diff):
  169. self.equipment.virtual_target_temperature = worst.temperature_target
  170. self.equipment.virtual_realtime_temperature = worst.realtime_temperature
  171. else:
  172. if worst.diff < 0:
  173. self.equipment.virtual_target_temperature = min(22.0, worst.temperature_target) + 0.5
  174. else:
  175. self.equipment.virtual_target_temperature = max(26.0, worst.temperature_target) - 0.5
  176. self.equipment.virtual_realtime_temperature = worst.realtime_temperature
  177. elif self.season == Season.heating:
  178. bad_spaces = sorted(bad_spaces, key=attrgetter("diff"), reverse=True)
  179. worst = bad_spaces[0]
  180. if worst.diff * virtual_diff >= 0:
  181. if abs(worst.diff) > abs(virtual_diff):
  182. self.equipment.virtual_target_temperature = worst.temperature_target
  183. self.equipment.virtual_realtime_temperature = worst.realtime_temperature
  184. else:
  185. if worst.diff > 0:
  186. self.equipment.virtual_target_temperature = max(26.0, worst.temperature_target) - 0.5
  187. else:
  188. self.equipment.virtual_target_temperature = min(20.0, worst.temperature_target) + 0.5
  189. self.equipment.virtual_realtime_temperature = worst.realtime_temperature
  190. return (
  191. self.equipment.virtual_target_temperature,
  192. self.equipment.virtual_realtime_temperature,
  193. )
  194. async def run(self) -> None:
  195. try:
  196. await self.build_virtual_temperature()
  197. temperature_set, temperature_realtime = await self.rectify()
  198. await self.get_supply_air_flow_set(temperature_set, temperature_realtime)
  199. self.equipment.running_status = True
  200. except TypeError:
  201. raise MissingIOTDataError
  202. class VAVControllerV3(VAVControllerV2):
  203. def __init__(self, vav_params: VAVBox, season: Season):
  204. super(VAVControllerV3, self).__init__(vav_params)
  205. self.season = season
  206. async def build_virtual_temperature(self) -> None:
  207. valid_spaces = self.get_valid_spaces()
  208. if not valid_spaces:
  209. raise MissingIOTDataError
  210. else:
  211. sorted_spaces = sorted(valid_spaces, key=lambda x: x.vav_temporary_update_time)
  212. if sorted_spaces[-1].vav_temporary_update_time > get_time_str(60 * 60 * 2, flag="ago"):
  213. virtual_realtime = sorted_spaces[-1].realtime_temperature
  214. virtual_target = sorted_spaces[-1].temperature_target
  215. else:
  216. virtual_realtime, virtual_target = 0.0, 0.0
  217. total_weight = 0.0
  218. for sp in valid_spaces:
  219. temp_weight = sp.vav_default_weight
  220. virtual_realtime += sp.realtime_temperature * temp_weight
  221. virtual_target += sp.temperature_target * temp_weight
  222. total_weight += temp_weight
  223. if total_weight == 0:
  224. for sp in valid_spaces:
  225. virtual_realtime += sp.realtime_temperature
  226. virtual_target += sp.temperature_target
  227. virtual_realtime /= len(valid_spaces)
  228. virtual_target /= len(valid_spaces)
  229. else:
  230. virtual_realtime /= total_weight
  231. virtual_target /= total_weight
  232. self.equipment.virtual_realtime_temperature = virtual_realtime
  233. self.equipment.virtual_target_temperature = virtual_target
  234. class VAVControllerV4(VAVControllerV3):
  235. def __init__(self, vav_params: VAVBox, season: Season, return_air_temp: float):
  236. super().__init__(vav_params, season)
  237. self.return_air_temp = return_air_temp
  238. def get_next_temp_set(self, virtual_realtime_temp: float, virtual_target_temp: float) -> float:
  239. if not (virtual_realtime_temp and virtual_target_temp):
  240. next_temp_set = np.NAN
  241. else:
  242. next_temp_set = virtual_target_temp + self.return_air_temp - virtual_realtime_temp
  243. self.equipment.setting_temperature = next_temp_set
  244. return next_temp_set
  245. async def run(self) -> None:
  246. try:
  247. await self.build_virtual_temperature()
  248. temperature_set, temperature_realtime = await self.rectify()
  249. self.get_next_temp_set(temperature_realtime, temperature_set)
  250. self.equipment.running_status = True
  251. except TypeError:
  252. raise MissingIOTDataError
  253. async def build_acatva_instructions(params: ACATVAInstructionsRequest) -> ACATVAInstructions:
  254. space_params = []
  255. for sp in params.spaces:
  256. temp_sp = SpaceATVA(**sp.dict())
  257. space_params.append(temp_sp)
  258. if params.supply_air_temperature:
  259. supply_air_temperature = params.supply_air_temperature
  260. else:
  261. supply_air_temperature = params.acatah_supply_air_temperature
  262. vav_params = VAVBox(
  263. spaces=space_params,
  264. supply_air_temperature=supply_air_temperature,
  265. supply_air_flow=params.supply_air_flow,
  266. supply_air_flow_lower_limit=params.supply_air_flow_lower_limit,
  267. supply_air_flow_upper_limit=params.supply_air_flow_upper_limit,
  268. )
  269. controller = VAVControllerV3(vav_params=vav_params, season=Season(params.season))
  270. await controller.run()
  271. regulated_vav = controller.get_results()
  272. instructions = ACATVAInstructions(
  273. supply_air_flow_set=regulated_vav.supply_air_flow_set,
  274. virtual_realtime_temperature=regulated_vav.virtual_realtime_temperature,
  275. virtual_temperature_target_set=regulated_vav.virtual_target_temperature,
  276. )
  277. return instructions
  278. async def build_acatva_instructions_for_jm(params: ACATVAInstructionsRequestV2) -> dict:
  279. # Control logic for Jiaming.
  280. space_params = []
  281. for sp in params.spaces:
  282. temp_sp = SpaceATVA(**sp.dict())
  283. space_params.append(temp_sp)
  284. vav_params = VAVBox(spaces=space_params)
  285. controller = VAVControllerV4(
  286. vav_params=vav_params,
  287. season=Season(params.season),
  288. return_air_temp=params.return_air_temperature,
  289. )
  290. await controller.run()
  291. regulated_vav = controller.get_results()
  292. next_temp_set = regulated_vav.setting_temperature
  293. if next_temp_set:
  294. if np.isnan(next_temp_set):
  295. next_temp_set = -1.0
  296. else:
  297. next_temp_set = -1.0
  298. instructions = {
  299. 'temperature_target_set': next_temp_set,
  300. 'virtual_target_temperature': regulated_vav.virtual_target_temperature,
  301. 'virtual_realtime_temperature': regulated_vav.virtual_realtime_temperature
  302. }
  303. return instructions