temperature.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. # -*- coding: utf-8 -*-
  2. from abc import ABCMeta, abstractmethod
  3. import arrow
  4. import numpy as np
  5. import pandas as pd
  6. from httpx import AsyncClient
  7. from loguru import logger
  8. from sqlalchemy.orm import Session
  9. from app.crud.space.weight import get_weights_by_space, update_weight
  10. from app.models.domain.feedback import FeedbackValue
  11. from app.resources.params import TEMPERATURE_TARGET_WEIGHT
  12. from app.schemas.sapce_weight import SpaceWeightUpdate
  13. from app.schemas.target import TemperatureTarget
  14. from app.services.platform import DataPlatformService
  15. from app.services.transfer import SpaceInfoService, Duoduo, Season
  16. from app.utils.date import get_time_str, TIME_FMT
  17. class StepSizeCalculator:
  18. """
  19. Calculate adjustment step size of environment target when a user send feedback.
  20. """
  21. def __init__(self, weight: dict[str, int]):
  22. self.weight = weight
  23. def run(self, realtime_temperature: float, comfortable_temperature: float, feedback: FeedbackValue) -> float:
  24. if feedback == FeedbackValue.so_hot or feedback == FeedbackValue.a_little_hot:
  25. base_step_size = 1.8 / (1 + np.exp((comfortable_temperature - realtime_temperature) / 2))
  26. else:
  27. base_step_size = 1.8 / (1 + np.exp((realtime_temperature - comfortable_temperature) / 2))
  28. return self.weight.get(str(feedback.value)) * base_step_size
  29. class SimpleStepSizeCalculator:
  30. """
  31. Zhijiang, this is for you!
  32. """
  33. def __init__(self):
  34. pass
  35. @staticmethod
  36. def run(feedback: FeedbackValue) -> float:
  37. if feedback == FeedbackValue.so_hot or feedback == FeedbackValue.a_little_hot:
  38. step_size = -1
  39. else:
  40. step_size = 1
  41. return step_size
  42. class NewTargetBuilder(metaclass=ABCMeta):
  43. """
  44. Calculate a new target value.
  45. """
  46. @abstractmethod
  47. def build(self) -> float:
  48. raise NotImplementedError
  49. class Clipper:
  50. """
  51. Return a number which is in the range of [min, max].
  52. """
  53. def __init__(self, upper_limit: float = 32.0, lower_limit: float = 16.0):
  54. self.upper_limit = upper_limit
  55. self.lower_limit = lower_limit
  56. def cut(self, num: float) -> float:
  57. num = min(num, self.upper_limit)
  58. num = max(num, self.lower_limit)
  59. return num
  60. class NewTemperatureTargetBuilder(NewTargetBuilder):
  61. """
  62. Calculate a new temperature target value.
  63. """
  64. def __init__(
  65. self, realtime_temperature: float, actual_target: float, step_size: float
  66. ):
  67. self.realtime_temperature = realtime_temperature
  68. self.actual_target = actual_target
  69. self.step_size = step_size
  70. def build(self) -> float:
  71. new_actual_target = self.actual_target
  72. if self.step_size > 0:
  73. if self.realtime_temperature + self.step_size > self.actual_target:
  74. new_actual_target = self.realtime_temperature + self.step_size
  75. elif self.step_size < 0:
  76. if self.realtime_temperature + self.step_size < self.actual_target:
  77. new_actual_target = self.realtime_temperature + self.step_size
  78. clipper = Clipper()
  79. return clipper.cut(new_actual_target)
  80. class NewTempTargetBuilderV2(NewTargetBuilder):
  81. """
  82. Calculate a new temperature target value for zhijiang.
  83. """
  84. def __init__(self, actual_target: float, step_sze: float):
  85. self.actual_target = actual_target
  86. self.step_size = step_sze
  87. def build(self) -> float:
  88. new_actual_target = self.actual_target + self.step_size
  89. clipper = Clipper()
  90. return clipper.cut(new_actual_target)
  91. class TemporaryTargetInit:
  92. """
  93. Initialize temporary temperature target.
  94. """
  95. def __init__(self, step_size: float, default_target: float = 24.0):
  96. self.step_size = step_size
  97. self.default_target = default_target
  98. def build(
  99. self,
  100. extent: float,
  101. season: Season,
  102. realtime_temperature: float,
  103. ) -> tuple[float, float]:
  104. if np.isnan(realtime_temperature):
  105. upper_bound, lower_bound = (
  106. self.default_target + 1.0,
  107. self.default_target - 1.0,
  108. )
  109. else:
  110. actual_target = np.NAN
  111. if season == Season.cooling:
  112. actual_target = realtime_temperature - self.step_size
  113. elif season == Season.heating:
  114. actual_target = realtime_temperature + self.step_size
  115. clipper = Clipper()
  116. actual_target = clipper.cut(actual_target)
  117. upper_bound, lower_bound = actual_target + (extent / 2), actual_target - (
  118. extent / 2
  119. )
  120. return lower_bound, upper_bound
  121. class GlobalTargetBaseBuilder(metaclass=ABCMeta):
  122. """
  123. Generate global target and format it for sending to TransferServer.
  124. """
  125. @abstractmethod
  126. def build(self, new_actual_target: float) -> dict:
  127. raise NotImplementedError
  128. class SimpleGlobalTemperatureTargetBuilder(GlobalTargetBaseBuilder):
  129. """
  130. Set all day temperature target same.
  131. """
  132. def __init__(self, current_global_target: TemperatureTarget):
  133. self.current_global_target = current_global_target
  134. def build(self, new_actual_target: float) -> dict:
  135. result = {}
  136. half_extent = self.current_global_target.extent / 2
  137. for time_index in self.current_global_target.target_schedule["temperatureMin"].keys():
  138. result.update({time_index: [new_actual_target - half_extent, new_actual_target + half_extent]})
  139. return result
  140. class ExpSmoothingTemperatureTargetBuilder(GlobalTargetBaseBuilder):
  141. """
  142. Exponential smooth previous changes and set them as new global target.
  143. """
  144. def __init__(
  145. self, current_global_target: TemperatureTarget, previous_changes: pd.DataFrame
  146. ):
  147. self.current_global_target = current_global_target
  148. self.previous_changes = previous_changes
  149. def build(self, new_actual_target: float) -> dict:
  150. now_time = arrow.get(get_time_str(), TIME_FMT).time().strftime("%H%M%S")
  151. half_extent = self.current_global_target.extent / 2
  152. previous_changes = pd.concat(
  153. [
  154. pd.DataFrame({"timestamp": [now_time], "value": [new_actual_target]}),
  155. self.previous_changes,
  156. ]
  157. )
  158. previous_changes.reset_index(inplace=True)
  159. previous_changes["weight1"] = previous_changes["index"].apply(
  160. lambda x: (1 / (x + 1)) ** 3
  161. )
  162. new_targets = {}
  163. time_index = self.current_global_target.target_schedule["temperatureMin"].keys()
  164. for item in time_index:
  165. previous_changes["delta"] = previous_changes["timestamp"].apply(
  166. lambda x: abs(arrow.get(str(x), "HHmmss") - arrow.get(item, "HHmmss")).seconds // (15 * 60)
  167. )
  168. previous_changes["weight2"] = previous_changes["delta"].apply(lambda x: 0.5 ** x)
  169. previous_changes["weight"] = (previous_changes["weight1"] * previous_changes["weight2"])
  170. temp_target = (
  171. previous_changes["value"] * previous_changes["weight"]
  172. ).sum() / previous_changes["weight"].sum()
  173. new_targets.update(
  174. {item: [temp_target - half_extent, temp_target + half_extent]}
  175. )
  176. return new_targets
  177. class TemporaryTargetBuilder:
  178. """
  179. Generate global target and format it for sending to TransferServer.
  180. """
  181. def __init__(self, lower_target: float, upper_target: float):
  182. self.lower_target = lower_target
  183. self.upper_target = upper_target
  184. def build(self) -> dict:
  185. now_str = get_time_str()
  186. time_index = (
  187. arrow.get(
  188. arrow.get(now_str, TIME_FMT).shift(minutes=15).timestamp()
  189. // (15 * 60)
  190. * (15 * 60)
  191. )
  192. .time()
  193. .strftime("%H%M%S")
  194. )
  195. return {time_index: [self.lower_target, self.upper_target]}
  196. class Carrier(metaclass=ABCMeta):
  197. """
  198. Fetch all you need data by one http client.
  199. """
  200. @abstractmethod
  201. async def fetch_all(self) -> None:
  202. raise NotImplementedError
  203. class Packer(metaclass=ABCMeta):
  204. """
  205. Arrange raw data for using.
  206. """
  207. @abstractmethod
  208. def run(self) -> dict:
  209. raise NotImplementedError
  210. class AdjustmentController(metaclass=ABCMeta):
  211. """
  212. Fetch some data, assemble target adjustment related functions and classes,
  213. send the new target to transfer server,
  214. and return a flag which denote whether transfer server need to request room/control.
  215. """
  216. @abstractmethod
  217. async def run(self) -> bool:
  218. raise NotImplementedError
  219. class TemperatureTargetCarrier(Carrier):
  220. """
  221. Fetch all the data that temperature target adjustment will use.
  222. """
  223. def __init__(self, project_id: str, object_id: str):
  224. self.project_id = project_id
  225. self.object_id = object_id
  226. self.result = {}
  227. async def fetch_all(self) -> None:
  228. async with AsyncClient() as client:
  229. transfer = SpaceInfoService(client, self.project_id, self.object_id)
  230. duoduo = Duoduo(client, self.project_id)
  231. platform = DataPlatformService(client, self.project_id)
  232. realtime_temperature = await platform.get_realtime_temperature(
  233. self.object_id
  234. )
  235. targets = await transfer.get_custom_target()
  236. all_day_targets = targets.get("normal_targets")
  237. current_target = await transfer.get_current_temperature_target()
  238. is_customized = await duoduo.is_customized(self.object_id)
  239. is_temporary = await transfer.is_temporary()
  240. season = await transfer.get_season()
  241. self.result = {
  242. "realtime_temperature": realtime_temperature,
  243. "all_day_targets": all_day_targets,
  244. "current_target": current_target,
  245. "is_customized": is_customized,
  246. "is_temporary": is_temporary,
  247. "season": season,
  248. }
  249. async def get_result(self) -> dict:
  250. await self.fetch_all()
  251. return self.result
  252. class TemperatureTargetV2Carrier(TemperatureTargetCarrier):
  253. """
  254. Add previous adjustment result to result.
  255. """
  256. async def fetch_previous_changes(self) -> None:
  257. async with AsyncClient() as client:
  258. transfer = SpaceInfoService(client, self.project_id, self.object_id)
  259. previous_changes = await transfer.env_database_get()
  260. self.result.update({"previous_changes": previous_changes["temperature"]})
  261. async def get_result(self) -> dict:
  262. await self.fetch_all()
  263. await self.fetch_previous_changes()
  264. return self.result
  265. class TemperatureTargetPacker:
  266. """
  267. Arrange raw data for temperature target adjustment.
  268. """
  269. def __init__(self, data):
  270. self.result = data
  271. def get_temperature_target(self):
  272. all_day_targets = self.result["all_day_targets"]
  273. if len(all_day_targets) > 0:
  274. extent = (
  275. all_day_targets["temperatureMax"].iloc[0]
  276. - all_day_targets["temperatureMin"].iloc[0]
  277. )
  278. temperature_all_day_targets = (
  279. all_day_targets[["temperatureMin", "temperatureMax"]].copy().to_dict()
  280. )
  281. else:
  282. extent = 2.0
  283. temperature_all_day_targets = {}
  284. target_params = {
  285. "is_customized": self.result["is_customized"],
  286. "is_temporary": self.result["is_temporary"],
  287. "target_schedule": temperature_all_day_targets,
  288. "extent": extent,
  289. }
  290. target = TemperatureTarget(**target_params)
  291. self.result.update({"target": target})
  292. def get_result(self) -> dict:
  293. self.get_temperature_target()
  294. return self.result
  295. class TargetDeliver:
  296. """
  297. Send target adjustment result to transfer.
  298. """
  299. def __init__(self, project_id: str, space_id: str):
  300. self.project_id = project_id
  301. self.space_id = space_id
  302. async def send(self, controlled_result: dict):
  303. async with AsyncClient() as client:
  304. transfer = SpaceInfoService(client, self.project_id, self.space_id)
  305. if controlled_result["need_switch_off"]:
  306. await transfer.set_temporary_custom()
  307. if controlled_result["new_temporary_target"]:
  308. transfer.set_custom_target(
  309. "temperature", controlled_result["new_temporary_target"], "0"
  310. )
  311. if controlled_result["new_global_target"]:
  312. transfer.set_custom_target(
  313. "temperature", controlled_result["new_global_target"], "1"
  314. )
  315. if (
  316. controlled_result["new_actual_target"] > 0
  317. and controlled_result["need_run_room_control"]
  318. ):
  319. await transfer.env_database_set(
  320. "temperature", controlled_result["new_actual_target"]
  321. )
  322. class WeightFlagDeliver:
  323. """
  324. Change a space temporary weight when the space receives a feedback about
  325. temperature.
  326. """
  327. def __init__(self, db: Session, feedback: FeedbackValue):
  328. self.db = db
  329. self.feedback = feedback
  330. def is_temperature_feedback(self) -> bool:
  331. if (
  332. self.feedback == FeedbackValue.a_little_hot
  333. or self.feedback == FeedbackValue.so_hot
  334. or self.feedback == FeedbackValue.a_little_cold
  335. or self.feedback == FeedbackValue.so_cold
  336. ):
  337. flag = True
  338. else:
  339. flag = False
  340. return flag
  341. def save(self, space: str):
  342. if self.is_temperature_feedback():
  343. weights = get_weights_by_space(self.db, space_id=space)
  344. for weight in weights:
  345. weight_in = SpaceWeightUpdate(temporary_weight=1.0)
  346. update_weight(self.db, db_weight=weight, weight_in=weight_in)
  347. class TemperatureTargetController:
  348. """
  349. Primary flow of temperature target adjustment for Sequoia.
  350. """
  351. def __init__(self, data: dict):
  352. self.data = data
  353. self.result = {}
  354. def run(self, feedback: FeedbackValue):
  355. need_switch_off = False
  356. new_temporary_target = {}
  357. new_global_target = {}
  358. new_actual_target = 0
  359. if feedback == FeedbackValue.switch_off:
  360. need_switch_off = True
  361. need_run_room_control = True
  362. elif feedback == FeedbackValue.switch_on:
  363. need_run_room_control = True
  364. if not self.data["is_customized"]:
  365. new_lower, new_upper = TemporaryTargetInit(1, 24).build(
  366. self.data["extent"],
  367. self.data["season"],
  368. self.data["realtime_temperature"],
  369. )
  370. new_temporary_target = TemporaryTargetBuilder(
  371. new_lower, new_upper
  372. ).build()
  373. elif (
  374. feedback == FeedbackValue.a_little_hot
  375. or feedback == FeedbackValue.a_little_cold
  376. or feedback == FeedbackValue.so_hot
  377. or feedback == FeedbackValue.so_cold
  378. ):
  379. step_size = StepSizeCalculator(TEMPERATURE_TARGET_WEIGHT).run(
  380. self.data["realtime_temperature"], 25.0, feedback
  381. )
  382. new_actual_target = NewTemperatureTargetBuilder(
  383. self.data["realtime_temperature"],
  384. self.data["current_target"],
  385. step_size,
  386. ).build()
  387. need_run_room_control = True
  388. if new_actual_target != self.data["current_target"]:
  389. new_global_target = SimpleGlobalTemperatureTargetBuilder(
  390. self.data["target"]
  391. ).build(new_actual_target)
  392. else:
  393. need_run_room_control = False
  394. self.result.update(
  395. {
  396. "need_switch_off": need_switch_off,
  397. "new_temporary_target": new_temporary_target,
  398. "new_global_target": new_global_target,
  399. "new_actual_target": new_actual_target,
  400. "need_run_room_control": need_run_room_control,
  401. }
  402. )
  403. def get_result(self) -> dict:
  404. return self.result
  405. class TemperatureTargetControllerV2:
  406. """
  407. Primary flow of temperature target adjustment for Zhonghai.
  408. """
  409. def __init__(self, data: dict):
  410. self.data = data
  411. self.result = {}
  412. def run(self, feedback: FeedbackValue):
  413. need_switch_off = False
  414. new_temporary_target = {}
  415. new_global_target = {}
  416. new_actual_target = 0
  417. if feedback == FeedbackValue.switch_off:
  418. need_switch_off = True
  419. need_run_room_control = True
  420. elif feedback == FeedbackValue.switch_on:
  421. need_run_room_control = True
  422. if not self.data["target"].is_customized:
  423. new_lower, new_upper = TemporaryTargetInit(1, 24).build(
  424. self.data["target"].extent,
  425. self.data["season"],
  426. self.data["realtime_temperature"],
  427. )
  428. new_temporary_target = TemporaryTargetBuilder(
  429. new_lower, new_upper
  430. ).build()
  431. elif (
  432. feedback == FeedbackValue.a_little_hot
  433. or feedback == FeedbackValue.a_little_cold
  434. or feedback == FeedbackValue.so_hot
  435. or feedback == FeedbackValue.so_cold
  436. ):
  437. step_size = StepSizeCalculator(TEMPERATURE_TARGET_WEIGHT).run(
  438. self.data["realtime_temperature"], 25.0, feedback
  439. )
  440. new_actual_target = NewTemperatureTargetBuilder(
  441. self.data["realtime_temperature"],
  442. self.data["current_target"],
  443. step_size,
  444. ).build()
  445. need_run_room_control = True
  446. if new_actual_target != self.data["current_target"]:
  447. new_global_target = ExpSmoothingTemperatureTargetBuilder(
  448. self.data["target"], self.data["previous_changes"]
  449. ).build(new_actual_target)
  450. else:
  451. need_run_room_control = False
  452. self.result.update(
  453. {
  454. "need_switch_off": need_switch_off,
  455. "new_temporary_target": new_temporary_target,
  456. "new_global_target": new_global_target,
  457. "new_actual_target": new_actual_target,
  458. "need_run_room_control": need_run_room_control,
  459. }
  460. )
  461. def get_result(self) -> dict:
  462. return self.result
  463. class TemperatureTargetControllerV3:
  464. """
  465. Primary flow of temperature target adjustment for Zhijiang.
  466. """
  467. def __init__(self, data: dict):
  468. self.data = data
  469. self.result = {}
  470. def run(self, feedback: FeedbackValue):
  471. need_switch_off = False
  472. new_temporary_target = {}
  473. new_global_target = {}
  474. new_actual_target = 0
  475. if feedback == FeedbackValue.switch_off:
  476. need_switch_off = True
  477. need_run_room_control = True
  478. elif feedback == FeedbackValue.switch_on:
  479. need_run_room_control = True
  480. if not self.data["is_customized"]:
  481. new_lower, new_upper = TemporaryTargetInit(1, 24).build(
  482. self.data["extent"],
  483. self.data["season"],
  484. self.data["realtime_temperature"],
  485. )
  486. new_temporary_target = TemporaryTargetBuilder(
  487. new_lower, new_upper
  488. ).build()
  489. elif (
  490. feedback == FeedbackValue.a_little_hot
  491. or feedback == FeedbackValue.a_little_cold
  492. or feedback == FeedbackValue.so_hot
  493. or feedback == FeedbackValue.so_cold
  494. ):
  495. step_size = SimpleStepSizeCalculator.run(feedback)
  496. new_actual_target = NewTempTargetBuilderV2(self.data["current_target"], step_size).build()
  497. need_run_room_control = True
  498. if new_actual_target != self.data["current_target"]:
  499. new_global_target = SimpleGlobalTemperatureTargetBuilder(
  500. self.data["target"]
  501. ).build(new_actual_target)
  502. else:
  503. need_run_room_control = False
  504. self.result.update(
  505. {
  506. "need_switch_off": need_switch_off,
  507. "new_temporary_target": new_temporary_target,
  508. "new_global_target": new_global_target,
  509. "new_actual_target": new_actual_target,
  510. "need_run_room_control": need_run_room_control,
  511. }
  512. )
  513. def get_result(self) -> dict:
  514. return self.result
  515. @logger.catch()
  516. async def temperature_target_control_v1(
  517. project_id: str, space_id: str, feedback: FeedbackValue
  518. ) -> bool:
  519. temperature_target_raw_data = await TemperatureTargetCarrier(
  520. project_id, space_id
  521. ).get_result()
  522. temperature_target_data = TemperatureTargetPacker(
  523. temperature_target_raw_data
  524. ).get_result()
  525. controller = TemperatureTargetController(temperature_target_data)
  526. controller.run(feedback)
  527. controlled_result = controller.get_result()
  528. await TargetDeliver(project_id, space_id).send(controlled_result)
  529. # WeightFlagDeliver(db, feedback).save(space_id)
  530. return controlled_result["need_run_room_control"]
  531. @logger.catch()
  532. async def temperature_target_control_v2(
  533. project_id: str, space_id: str, feedback: FeedbackValue
  534. ) -> bool:
  535. temperature_target_raw_data = await TemperatureTargetV2Carrier(
  536. project_id, space_id
  537. ).get_result()
  538. temperature_target_data = TemperatureTargetPacker(
  539. temperature_target_raw_data
  540. ).get_result()
  541. controller = TemperatureTargetControllerV2(temperature_target_data)
  542. controller.run(feedback)
  543. controlled_result = controller.get_result()
  544. await TargetDeliver(project_id, space_id).send(controlled_result)
  545. return controlled_result["need_run_room_control"]
  546. @logger.catch()
  547. async def temperature_target_control_v3(
  548. project_id: str, space_id: str, feedback: FeedbackValue
  549. ) -> bool:
  550. temperature_target_raw_data = await TemperatureTargetCarrier(
  551. project_id, space_id
  552. ).get_result()
  553. temperature_target_data = TemperatureTargetPacker(
  554. temperature_target_raw_data
  555. ).get_result()
  556. controller = TemperatureTargetControllerV3(temperature_target_data)
  557. controller.run(feedback)
  558. controlled_result = controller.get_result()
  559. await TargetDeliver(project_id, space_id).send(controlled_result)
  560. return controlled_result["need_run_room_control"]
  561. @logger.catch()
  562. async def get_target_after_feedback(
  563. project_id: str, space_id: str, feedback: FeedbackValue
  564. ) -> float:
  565. if (project_id == "Pj1101050030" or project_id == "Pj1101140020" or project_id == "Pj1101050039"
  566. or project_id == "Pj3301100002"):
  567. temperature_target_raw_data = await TemperatureTargetCarrier(
  568. project_id, space_id
  569. ).get_result()
  570. else:
  571. temperature_target_raw_data = await TemperatureTargetV2Carrier(
  572. project_id, space_id
  573. ).get_result()
  574. temperature_target_data = TemperatureTargetPacker(
  575. temperature_target_raw_data
  576. ).get_result()
  577. if project_id == "Pj1101050030" or project_id == 'Pj1101140020' or project_id == 'Pj1101050039':
  578. controller = TemperatureTargetController(temperature_target_data)
  579. elif project_id == "Pj3301100002":
  580. controller = TemperatureTargetControllerV3(temperature_target_data)
  581. else:
  582. controller = TemperatureTargetControllerV2(temperature_target_data)
  583. controller.run(feedback)
  584. controlled_result = controller.get_result()
  585. return controlled_result.get("new_actual_target")