target.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. # -*- coding: utf-8 -*-
  2. from abc import abstractmethod
  3. from typing import Dict, Tuple, Optional
  4. import arrow
  5. import numpy as np
  6. import pandas as pd
  7. from httpx import AsyncClient
  8. from loguru import logger
  9. from app.controllers.controller import Controller
  10. from app.resources.params import (
  11. TEMPERATURE_RELATED_FEEDBACK_WEIGHT,
  12. TEMPERATURE_RELATED_FEEDBACK,
  13. CO2_RELATED_FEEDBACK_WEIGHT,
  14. SWITCH_RELATED_FEEDBACK
  15. )
  16. from app.services.platform import DataPlatformService
  17. from app.services.transfer import SpaceInfoService, Season
  18. from app.utils.date import get_time_str, get_quarter_minutes, TIME_FMT
  19. class TargetController(Controller):
  20. def __init__(
  21. self,
  22. realtime_data: float,
  23. feedback: Dict,
  24. is_customized: bool,
  25. is_temporary: bool,
  26. current_targets: pd.DataFrame,
  27. ) -> None:
  28. super(TargetController, self).__init__()
  29. self._realtime_data = realtime_data
  30. self._feedback = feedback
  31. self._is_customized = is_customized
  32. self._is_temporary = is_temporary
  33. self._current_targets = current_targets
  34. self._now_time = arrow.get(get_time_str(), TIME_FMT).time().strftime('%H%M%S')
  35. self._quarter_time = get_quarter_minutes(get_time_str())
  36. async def calculate_diff(self, weight: Dict) -> float:
  37. related_feedback = [v for k, v in self._feedback.items() if k in weight]
  38. related_feedback = np.array(related_feedback)
  39. weight = np.array(list(weight.values()))
  40. feedback_count = related_feedback.sum()
  41. diff = 0
  42. if feedback_count > 0:
  43. diff = np.dot(related_feedback, weight) / feedback_count
  44. return diff
  45. @abstractmethod
  46. async def init_temporary(self):
  47. pass
  48. @abstractmethod
  49. async def get_targets(self) -> float:
  50. pass
  51. async def generate_temporary(self, lower, upper):
  52. now_str = get_time_str()
  53. time_index = arrow.get(arrow.get(now_str, TIME_FMT).shift(minutes=15).timestamp
  54. // (15 * 60) * (15 * 60)).time().strftime('%H%M%S')
  55. result = {time_index: [lower, upper]}
  56. self._results.update({'temporary_targets': result})
  57. async def readjust_global(self, latest_change: float, previous_changes: pd.DataFrame):
  58. previous_changes = pd.concat([
  59. pd.DataFrame({'timestamp': [self._now_time], 'value': [latest_change]}),
  60. previous_changes,
  61. ])
  62. previous_changes.reset_index(inplace=True)
  63. previous_changes['weight1'] = previous_changes['index'].apply(lambda x: (1 / (x + 1)) ** 3)
  64. new_targets = []
  65. time_index = self._current_targets.reset_index()['time']
  66. for item in time_index:
  67. previous_changes['delta'] = previous_changes['timestamp'].apply(
  68. lambda x: abs(arrow.get(str(x), 'HHmmss') - arrow.get(item, 'HHmmss')).seconds // (15 * 60)
  69. )
  70. previous_changes['weight2'] = previous_changes['delta'].apply(lambda x: 0.5 ** x)
  71. previous_changes['weight'] = previous_changes['weight1'] * previous_changes['weight2']
  72. new_targets.append(
  73. (previous_changes['value'] * previous_changes['weight']).sum() / previous_changes['weight'].sum()
  74. )
  75. self._current_targets['new_targets'] = new_targets
  76. @abstractmethod
  77. async def run(self):
  78. pass
  79. class TemperatureTargetController(TargetController):
  80. def __init__(
  81. self,
  82. realtime_data: float,
  83. feedback: Dict,
  84. is_customized: bool,
  85. is_temporary: bool,
  86. current_targets: pd.DataFrame,
  87. season: Season,
  88. previous_changes: Optional[pd.DataFrame] = None
  89. ) -> None:
  90. super(TemperatureTargetController, self).__init__(
  91. realtime_data,
  92. feedback,
  93. is_customized,
  94. is_temporary,
  95. current_targets
  96. )
  97. self._season = season
  98. self._previous_changes = previous_changes
  99. @staticmethod
  100. def _cut(value: float) -> float:
  101. _LOWER_LIMIT = 22.0
  102. _UPPER_LIMIT = 28.0
  103. value = min(value, _UPPER_LIMIT)
  104. value = max(value, _LOWER_LIMIT)
  105. return value
  106. async def init_temporary(self) -> Tuple[float, float]:
  107. _VAR = 2
  108. _RANGE = 1
  109. new_target = 24.0
  110. new_lower_bound, new_upper_bound = new_target - 1.0, new_target + 1.0
  111. if not np.isnan(self._realtime_data):
  112. if self._season == Season.cooling:
  113. if ('a little hot' in self._feedback
  114. or 'so hot' in self._feedback
  115. or 'switch on' in self._feedback):
  116. mid = self._realtime_data - _VAR
  117. mid = self._cut(mid)
  118. new_lower_bound = mid - _RANGE
  119. new_upper_bound = mid + _RANGE
  120. elif self._season == Season.heating:
  121. if ('a little cold' in self._feedback
  122. or 'so cold' in self._feedback
  123. or 'switch on' in self._feedback):
  124. mid = self._realtime_data + _VAR
  125. mid = self._cut(mid)
  126. new_lower_bound = mid - _RANGE
  127. new_upper_bound = mid + _RANGE
  128. return new_lower_bound, new_upper_bound
  129. async def get_targets(self) -> Tuple[float, float]:
  130. current_lower_target = self._current_targets['temperatureMin'].loc[self._quarter_time]
  131. current_upper_target = self._current_targets['temperatureMax'].loc[self._quarter_time]
  132. # if np.isnan(current_lower_target):
  133. # current_lower_target = 23.0
  134. # if np.isnan(current_upper_target):
  135. # current_upper_target = 25.0
  136. return (current_lower_target + current_upper_target) / 2, current_upper_target - current_lower_target
  137. async def readjust_current(self, current_target: float, extent: float, diff: float) -> float:
  138. # _RANGE = 2
  139. new_target = current_target
  140. if not np.isnan(self._realtime_data):
  141. if (diff < 0 and self._realtime_data > current_target + extent / 2
  142. or diff > 0 and self._realtime_data < current_target - extent / 2):
  143. new_target = current_target
  144. else:
  145. new_target = self._realtime_data + diff
  146. return new_target
  147. async def generate_global(self, extent: float):
  148. _RANGE = 1
  149. new_targets = self._current_targets['new_targets'].apply(lambda x: [self._cut(x) - extent / 2,
  150. self._cut(x) + extent / 2])
  151. time_index = self._current_targets.reset_index()['time']
  152. result = {}
  153. for i in range(len(time_index)):
  154. result.update({time_index[i]: new_targets[i]})
  155. self._results.update({'global_targets': result})
  156. async def run(self):
  157. diff = await self.calculate_diff(TEMPERATURE_RELATED_FEEDBACK_WEIGHT)
  158. if diff != 0:
  159. if not self._is_customized:
  160. lower_bound, upper_bound = await self.init_temporary()
  161. await self.generate_temporary(lower_bound, upper_bound)
  162. else:
  163. current_target, extent = await self.get_targets()
  164. new_target = await self.readjust_current(current_target, extent, diff)
  165. new_target = self._cut(new_target)
  166. if not self._is_temporary:
  167. self._results.update({'latest_change': new_target})
  168. await self.readjust_global(new_target, self._previous_changes)
  169. await self.generate_global(extent)
  170. else:
  171. await self.generate_temporary(new_target - 1.0, new_target + 1.0)
  172. else:
  173. return
  174. class Co2TargetController(TargetController):
  175. def __init__(
  176. self,
  177. realtime_data: float,
  178. feedback: Dict,
  179. is_customized: bool,
  180. is_temporary: bool,
  181. current_targets: pd.DataFrame,
  182. previous_changes: Optional[pd.DataFrame] = None
  183. ) -> None:
  184. super(Co2TargetController, self).__init__(
  185. realtime_data,
  186. feedback,
  187. is_customized,
  188. is_temporary,
  189. current_targets
  190. )
  191. self._previous_changes = previous_changes
  192. @staticmethod
  193. def _cut(value: float) -> float:
  194. _UPPER_LIMIT = 1000.0
  195. value = min(value, _UPPER_LIMIT)
  196. return value
  197. async def init_temporary(self) -> float:
  198. new_target = 1000
  199. diff = await self.calculate_diff(CO2_RELATED_FEEDBACK_WEIGHT)
  200. if not np.isnan(self._realtime_data):
  201. new_target += diff
  202. return self._cut(new_target)
  203. async def get_targets(self) -> float:
  204. current_upper_target = self._current_targets['co2Max'].loc[self._quarter_time]
  205. if np.isnan(current_upper_target):
  206. current_upper_target = 500.0
  207. return current_upper_target
  208. async def readjust_current(self, lower: float, upper: float, diff: float) -> float:
  209. new_target = upper - lower
  210. if np.isnan(self._realtime_data):
  211. new_target += diff
  212. else:
  213. if (diff > 50 and self._realtime_data + 100 > upper
  214. or diff < -50 and self._realtime_data - 100 < upper):
  215. new_target = self._realtime_data + diff
  216. return self._cut(new_target)
  217. async def generate_global(self):
  218. new_targets = self._current_targets['new_targets'].apply(lambda x: [0, x])
  219. time_index = self._current_targets.reset_index()['time']
  220. result = {}
  221. for i in range(len(time_index)):
  222. result.update({time_index[i]: new_targets[i]})
  223. self._results.update({'global_targets': result})
  224. async def run(self):
  225. diff = await self.calculate_diff(CO2_RELATED_FEEDBACK_WEIGHT)
  226. if diff != 0:
  227. if not self._is_customized:
  228. upper_bound = await self.init_temporary()
  229. await self.generate_temporary(0, upper_bound)
  230. else:
  231. current_upper = await self.get_targets()
  232. upper_bound = await self.readjust_current(0, current_upper, diff)
  233. if not self._is_temporary:
  234. self._results.update({'latest_change': upper_bound})
  235. await self.readjust_global(upper_bound, self._previous_changes)
  236. await self.generate_global()
  237. else:
  238. await self.generate_temporary(0, upper_bound)
  239. else:
  240. return
  241. @logger.catch()
  242. async def readjust_all_target(
  243. project_id: str,
  244. space_id: str,
  245. wechat_time: Optional[str] = None,
  246. feedback: Optional[Dict] = None
  247. ):
  248. async with AsyncClient() as client:
  249. transfer = SpaceInfoService(client, project_id, space_id)
  250. platform = DataPlatformService(client, project_id)
  251. realtime_temperature = await platform.get_realtime_temperature(space_id)
  252. current_targets = await transfer.get_custom_target()
  253. if wechat_time:
  254. feedback = await transfer.get_feedback(wechat_time)
  255. is_customized = await transfer.is_customized()
  256. is_temporary = await transfer.is_temporary()
  257. season = await transfer.get_season()
  258. previous_changes = await transfer.env_database_get()
  259. if feedback.get('switch off') and feedback.get('switch off') > 0:
  260. need_switch_off = True
  261. for item in SWITCH_RELATED_FEEDBACK:
  262. if feedback.get(item) and feedback.get(item) > 0:
  263. need_switch_off = False
  264. break
  265. else:
  266. need_switch_off = False
  267. need_run_room_control = False
  268. if need_switch_off:
  269. need_run_room_control = True
  270. async with AsyncClient() as client:
  271. transfer = SpaceInfoService(client, project_id, space_id)
  272. await transfer.set_temporary_custom()
  273. return need_run_room_control
  274. temperature_results = {}
  275. for item in TEMPERATURE_RELATED_FEEDBACK:
  276. if feedback.get(item) and feedback.get(item) > 0:
  277. temperature_controller = TemperatureTargetController(
  278. realtime_temperature,
  279. feedback,
  280. is_customized,
  281. is_temporary,
  282. current_targets[['temperatureMin', 'temperatureMax']].copy(),
  283. season,
  284. previous_changes['temperature']
  285. )
  286. await temperature_controller.run()
  287. temperature_results = temperature_controller.get_results()
  288. break
  289. if temperature_results:
  290. need_run_room_control = True
  291. async with AsyncClient() as client:
  292. transfer = SpaceInfoService(client, project_id, space_id)
  293. if temperature_results.get('temporary_targets'):
  294. await transfer.set_custom_target('temperature', temperature_results.get('temporary_targets'), '0')
  295. if temperature_results.get('global_targets'):
  296. await transfer.set_custom_target('temperature', temperature_results.get('global_targets'), '1')
  297. if temperature_results.get('latest_change'):
  298. await transfer.env_database_set('temperature', temperature_results.get('latest_change'))
  299. return need_run_room_control