vav.py 14 KB

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