temperature.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  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.models.domain.feedback import FeedbackValue
  9. from app.resources.params import TEMPERATURE_TARGET_WEIGHT
  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: FeedbackValue
  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. feedback: Optional[FeedbackValue] = None
  136. ):
  137. self.project_id = project_id
  138. self.space_id = space_id
  139. self.feedback = feedback
  140. @logger.catch()
  141. async def run(self) -> bool:
  142. # Fetch all data from data platform and transfer server.
  143. async with AsyncClient() as client:
  144. transfer = SpaceInfoService(client, self.project_id, self.space_id)
  145. platform = DataPlatformService(client, self.project_id)
  146. realtime_temperature = await platform.get_realtime_temperature(self.space_id)
  147. logger.debug(f'realtime temperature: {realtime_temperature}')
  148. all_day_targets = await transfer.get_custom_target()
  149. current_target = await transfer.get_current_temperature_target()
  150. logger.debug(f'current target: {current_target}')
  151. is_customized = await transfer.is_customized()
  152. logger.debug(f'is customized? {is_customized}')
  153. is_temporary = await transfer.is_temporary()
  154. logger.debug(f'is temporary? {is_temporary}')
  155. season = await transfer.get_season()
  156. # Arrange data
  157. if len(all_day_targets) > 0:
  158. extent = all_day_targets['temperatureMax'].iloc[0] - all_day_targets['temperatureMin'].iloc[0]
  159. else:
  160. extent = 2.0
  161. temperature_all_day_targets = all_day_targets[['temperatureMin', 'temperatureMax']].copy()
  162. target_params = {
  163. 'is_customized': is_customized,
  164. 'is_temporary': is_temporary,
  165. 'target_schedule': temperature_all_day_targets.to_dict(),
  166. 'extent': extent
  167. }
  168. target = TemperatureTarget(**target_params)
  169. # Primary flow
  170. need_switch_off = False
  171. new_temporary_target = {}
  172. new_global_target = {}
  173. new_actual_target = 0
  174. if self.feedback == FeedbackValue.switch_off:
  175. need_switch_off = True
  176. need_run_room_control = True
  177. elif self.feedback == FeedbackValue.switch_on:
  178. need_run_room_control = True
  179. if not is_customized:
  180. new_lower, new_upper = TemporaryTargetInit(1, 24).build(extent, season, realtime_temperature)
  181. new_temporary_target = TemporaryTargetBuilder(new_lower, new_upper)
  182. elif (self.feedback == FeedbackValue.a_little_hot
  183. or self.feedback == FeedbackValue.a_little_cold
  184. or self.feedback == FeedbackValue.so_hot
  185. or self.feedback == FeedbackValue.so_cold):
  186. step_size = StepSizeCalculator(TEMPERATURE_TARGET_WEIGHT).run(realtime_temperature, 25.0, self.feedback)
  187. logger.debug(f'step size: {step_size}')
  188. new_actual_target = NewTemperatureTargetBuilder(realtime_temperature, current_target, step_size).build()
  189. if new_actual_target != current_target:
  190. new_global_target = SimpleGlobalTemperatureTargetBuilder(target).build(new_actual_target)
  191. need_run_room_control = True
  192. else:
  193. need_run_room_control = False
  194. else:
  195. need_run_room_control = False
  196. # Send adjustment result to transfer server
  197. async with AsyncClient() as client:
  198. transfer = SpaceInfoService(client, self.project_id, self.space_id)
  199. if need_switch_off:
  200. await transfer.set_temporary_custom()
  201. logger.debug(f'turn off')
  202. if new_temporary_target:
  203. await transfer.set_custom_target('temperature', new_temporary_target, '0')
  204. logger.debug(f'new temporary targets: {new_temporary_target}')
  205. if new_global_target:
  206. await transfer.set_custom_target('temperature', new_global_target, '1')
  207. logger.debug(f'new global targets: {new_global_target}')
  208. if new_actual_target != current_target:
  209. await transfer.env_database_set('temperature', new_actual_target)
  210. logger.debug(f'new actual target: {new_actual_target}')
  211. return need_run_room_control