temperature.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. # -*- coding: utf-8 -*-
  2. from abc import ABCMeta, abstractmethod
  3. from typing import Dict, Optional, Tuple
  4. import arrow
  5. import numpy as np
  6. from httpx import AsyncClient
  7. from loguru import logger
  8. from app.resources.params import TEMPERATURE_TARGET_WEIGHT
  9. from app.schemas.feedback import Feedback
  10. from app.schemas.target import TemperatureTarget
  11. from app.services.platform import DataPlatformService
  12. from app.services.transfer import SpaceInfoService, Season
  13. from app.utils.date import get_time_str, TIME_FMT
  14. class StepSizeCalculator:
  15. """
  16. Calculate adjustment step size of environment target when a user send feedback.
  17. """
  18. def __init__(self, weight: Dict):
  19. self.weight = weight
  20. def run(
  21. self,
  22. realtime_temperature: float,
  23. comfortable_temperature: float,
  24. feedback: Feedback
  25. ) -> float:
  26. base_step_size = 1.8 / (1 + np.exp((comfortable_temperature - realtime_temperature) / 2))
  27. return self.weight.get(feedback.value) * base_step_size
  28. class NewTargetBuilder(metaclass=ABCMeta):
  29. """
  30. Calculate a new target value.
  31. """
  32. @abstractmethod
  33. def build(self) -> float:
  34. raise NotImplementedError
  35. class NewTemperatureTargetBuilder(NewTargetBuilder):
  36. """
  37. Calculate a new temperature target value.
  38. """
  39. def __init__(self, realtime_temperature: float, actual_target: float, step_size: float):
  40. self.realtime_temperature = realtime_temperature
  41. self.actual_target = actual_target
  42. self.step_size = step_size
  43. def build(self) -> float:
  44. new_actual_target = self.actual_target
  45. if self.step_size > 0:
  46. if self.realtime_temperature + self.step_size > self.actual_target:
  47. new_actual_target = self.realtime_temperature + self.step_size
  48. elif self.step_size < 0:
  49. if self.realtime_temperature + self.step_size < self.actual_target:
  50. new_actual_target = self.realtime_temperature + self.step_size
  51. return new_actual_target
  52. class Clipper:
  53. """
  54. Return a number which is in the range of [min, max].
  55. """
  56. def __init__(self, upper_limit: float = 28.0, lower_limit: float = 22.0):
  57. self.upper_limit = upper_limit
  58. self.lower_limit = lower_limit
  59. def cut(self, num: float) -> float:
  60. num = min(num, self.upper_limit)
  61. num = max(num, self.lower_limit)
  62. return num
  63. class TemporaryTargetInit:
  64. """
  65. Initialize temporary temperature target.
  66. """
  67. def __init__(self, step_size: float, default_target: float = 24.0):
  68. self.step_size = step_size
  69. self.default_target = default_target
  70. def build(
  71. self,
  72. extent: float,
  73. season: Season,
  74. realtime_temperature: float,
  75. ) -> Tuple[float, float]:
  76. if np.isnan(realtime_temperature):
  77. upper_bound, lower_bound = self.default_target + 1.0, self.default_target - 1.0
  78. else:
  79. actual_target = np.NAN
  80. if season == Season.cooling:
  81. actual_target = realtime_temperature - self.step_size
  82. elif season == Season.heating:
  83. actual_target = realtime_temperature + self.step_size
  84. clipper = Clipper()
  85. actual_target = clipper.cut(actual_target)
  86. upper_bound, lower_bound = actual_target + (extent / 2), actual_target - (extent / 2)
  87. return lower_bound, upper_bound
  88. class GlobalTargetBaseBuilder(metaclass=ABCMeta):
  89. """
  90. Generate global target and format it for sending to TransferServer.
  91. """
  92. @abstractmethod
  93. def build(self, new_actual_target: float) -> Dict:
  94. raise NotImplementedError
  95. class SimpleGlobalTemperatureTargetBuilder(GlobalTargetBaseBuilder):
  96. """
  97. Set all day temperature target same.
  98. """
  99. def __init__(self, current_global_target: TemperatureTarget):
  100. self.current_global_target = current_global_target
  101. def build(self, new_actual_target: float) -> Dict:
  102. result = {}
  103. half_extent = self.current_global_target.extent / 2
  104. for time_index in self.current_global_target.target_schedule['temperatureMin'].keys():
  105. result.update({time_index: [new_actual_target - half_extent, new_actual_target + half_extent]})
  106. return result
  107. class TemporaryTargetBuilder:
  108. """
  109. Generate global target and format it for sending to TransferServer.
  110. """
  111. def __init__(self, lower_target: float, upper_target: float):
  112. self.lower_target = lower_target
  113. self.upper_target = upper_target
  114. def build(self) -> Dict:
  115. now_str = get_time_str()
  116. time_index = arrow.get(arrow.get(now_str, TIME_FMT).shift(minutes=15).timestamp
  117. // (15 * 60) * (15 * 60)).time().strftime('%H%M%S')
  118. return {time_index: [self.lower_target, self.upper_target]}
  119. class AdjustmentController(metaclass=ABCMeta):
  120. """
  121. Fetch some data, assemble target adjustment related functions and classes, send the new target to transfer server,
  122. and return a flag which denote whether transfer server need to request room/control.
  123. """
  124. @abstractmethod
  125. async def run(self) -> bool:
  126. raise NotImplementedError
  127. class TemperatureAdjustmentController(AdjustmentController):
  128. """
  129. Test strategy for Sequoia.
  130. """
  131. def __init__(
  132. self,
  133. project_id: str,
  134. space_id: str,
  135. wechat_time: Optional[str] = None,
  136. raw_feedback: Optional[Dict] = None
  137. ):
  138. self.project_id = project_id
  139. self.space_id = space_id
  140. self.wechat_time = wechat_time
  141. self.raw_feedback = raw_feedback
  142. @logger.catch()
  143. async def run(self) -> bool:
  144. # Fetch all data from data platform and transfer server.
  145. async with AsyncClient() as client:
  146. transfer = SpaceInfoService(client, self.project_id, self.space_id)
  147. platform = DataPlatformService(client, self.project_id)
  148. realtime_temperature = await platform.get_realtime_temperature(self.space_id)
  149. logger.debug(f'realtime temperature: {realtime_temperature}')
  150. all_day_targets = await transfer.get_custom_target()
  151. current_target = await transfer.get_current_temperature_target()
  152. logger.debug(f'current target: {current_target}')
  153. if self.wechat_time:
  154. self.raw_feedback = await transfer.get_feedback(self.wechat_time)
  155. logger.debug(f'raw feedback: {self.raw_feedback}')
  156. is_customized = await transfer.is_customized()
  157. logger.debug(f'is customized? {is_customized}')
  158. is_temporary = await transfer.is_temporary()
  159. logger.debug(f'is temporary? {is_temporary}')
  160. season = await transfer.get_season()
  161. # Arrange data.
  162. feedback = Feedback('nice')
  163. for k, v in self.raw_feedback.items():
  164. if v > 0:
  165. feedback = Feedback(k)
  166. break
  167. logger.debug(feedback)
  168. if len(all_day_targets) > 0:
  169. extent = all_day_targets['temperatureMax'].iloc[0] - all_day_targets['temperatureMin'].iloc[0]
  170. else:
  171. extent = 2.0
  172. temperature_all_day_targets = all_day_targets[['temperatureMin', 'temperatureMax']].copy()
  173. target_params = {
  174. 'is_customized': is_customized,
  175. 'is_temporary': is_temporary,
  176. 'target_schedule': temperature_all_day_targets.to_dict(),
  177. 'extent': extent
  178. }
  179. target = TemperatureTarget(**target_params)
  180. # Primary flow
  181. need_switch_off = False
  182. new_temporary_target = {}
  183. new_global_target = {}
  184. new_actual_target = 0
  185. if feedback == Feedback.switch_off:
  186. need_switch_off = True
  187. need_run_room_control = True
  188. elif feedback == Feedback.switch_on:
  189. need_run_room_control = True
  190. if not is_customized:
  191. new_lower, new_upper = TemporaryTargetInit(1, 24).build(extent, season, realtime_temperature)
  192. new_temporary_target = TemporaryTargetBuilder(new_lower, new_upper)
  193. elif (feedback == Feedback.a_little_hot
  194. or feedback == Feedback.a_little_cold
  195. or feedback == Feedback.so_hot
  196. or feedback == Feedback.so_cold):
  197. step_size = StepSizeCalculator(TEMPERATURE_TARGET_WEIGHT).run(realtime_temperature, 25.0, feedback)
  198. logger.debug(f'step size: {step_size}')
  199. new_actual_target = NewTemperatureTargetBuilder(realtime_temperature, current_target, step_size).build()
  200. if new_actual_target != current_target:
  201. new_global_target = SimpleGlobalTemperatureTargetBuilder(target).build(new_actual_target)
  202. need_run_room_control = True
  203. else:
  204. need_run_room_control = False
  205. else:
  206. need_run_room_control = False
  207. # Send adjustment result to transfer server
  208. async with AsyncClient() as client:
  209. transfer = SpaceInfoService(client, self.project_id, self.space_id)
  210. if need_switch_off:
  211. await transfer.set_temporary_custom()
  212. logger.debug(f'turn off')
  213. if new_temporary_target:
  214. await transfer.set_custom_target('temperature', new_temporary_target, '0')
  215. logger.debug(f'new temporary targets: {new_temporary_target}')
  216. if new_global_target:
  217. await transfer.set_custom_target('temperature', new_global_target, '1')
  218. logger.debug(f'new global targets: {new_global_target}')
  219. if new_actual_target != current_target:
  220. await transfer.env_database_set('temperature', new_actual_target)
  221. logger.debug(f'new actual target: {new_actual_target}')
  222. return need_run_room_control