ソースを参照

refactor temperature readjustment for Zhonghai

chenhaiyang 4 年 前
コミット
128c2d1e97
2 ファイル変更282 行追加67 行削除
  1. 9 11
      app/api/routers/targets.py
  2. 273 56
      app/controllers/targets/temperature.py

+ 9 - 11
app/api/routers/targets.py

@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException, Query
 from loguru import logger
 
 from app.controllers.targets.target import readjust_all_target
-from app.controllers.targets.temperature import TemperatureAdjustmentController
+from app.controllers.targets.temperature import temperature_target_control_v1, temperature_target_control_v2
 from app.models.domain.feedback import FeedbackValue
 from app.models.domain.targets import TargetReadjustResponse
 from app.utils.date import get_time_str
@@ -21,17 +21,16 @@ async def readjust_target(
         timestamp: Optional[str] = Query(None, min_length=14, max_length=14, alias='time'),
         feedback: Optional[FeedbackValue] = Query(None, alias='itemId')
 ):
+    if not timestamp:
+        timestamp = get_time_str()
     try:
-        if project_id == 'Pj1101050030':
-            if feedback != FeedbackValue.null:
-                controller = TemperatureAdjustmentController(project_id, space_id, feedback)
-                need_run_room_control = await controller.run()
+        if feedback != FeedbackValue.null:
+            if project_id == 'Pj1101050030':
+                need_run_room_control = await temperature_target_control_v1(project_id, space_id, feedback)
             else:
-                need_run_room_control = True
+                need_run_room_control = await temperature_target_control_v2(project_id, space_id, feedback)
         else:
-            if not timestamp:
-                timestamp = get_time_str()
-            need_run_room_control = await readjust_all_target(project_id, space_id, timestamp)
+            need_run_room_control = True
     except Exception as e:
         logger.error(e)
         raise HTTPException(
@@ -86,8 +85,7 @@ async def readjust_target_v2(
 ):
     if feedback != FeedbackValue.null:
         try:
-            controller = TemperatureAdjustmentController(project_id, space_id, feedback)
-            need_run_room_control = await controller.run()
+            need_run_room_control = await temperature_target_control_v2(project_id, space_id, feedback)
         except Exception as e:
             logger.error(e)
             raise HTTPException(

+ 273 - 56
app/controllers/targets/temperature.py

@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
 
 from abc import ABCMeta, abstractmethod
-from typing import Dict, Optional, Tuple
+from typing import Dict, Tuple
 
 import arrow
 import numpy as np
+import pandas as pd
 from httpx import AsyncClient
 from loguru import logger
 
@@ -138,6 +139,39 @@ class SimpleGlobalTemperatureTargetBuilder(GlobalTargetBaseBuilder):
         return result
 
 
+class ExpSmoothingTemperatureTargetBuilder(GlobalTargetBaseBuilder):
+    """
+    Exponential smooth previous changes and set them as new global target.
+    """
+
+    def __init__(self, current_global_target: TemperatureTarget, previous_changes: pd.DataFrame):
+        self.current_global_target = current_global_target
+        self.previous_changes = previous_changes
+
+    def build(self, new_actual_target: float) -> Dict:
+        now_time = arrow.get(get_time_str(), TIME_FMT).time().strftime('%H%M%S')
+        half_extent = self.current_global_target.extent / 2
+        previous_changes = pd.concat([
+            pd.DataFrame({'timestamp': [now_time], 'value': [new_actual_target]}),
+            self.previous_changes,
+        ])
+        previous_changes.reset_index(inplace=True)
+        previous_changes['weight1'] = previous_changes['index'].apply(lambda x: (1 / (x + 1)) ** 3)
+        new_targets = {}
+        time_index = self.current_global_target.target_schedule['temperatureMin'].keys()
+        for item in time_index:
+            previous_changes['delta'] = previous_changes['timestamp'].apply(
+                lambda x: abs(arrow.get(str(x), 'HHmmss') - arrow.get(item, 'HHmmss')).seconds // (15 * 60)
+            )
+            previous_changes['weight2'] = previous_changes['delta'].apply(lambda x: 0.5 ** x)
+            previous_changes['weight'] = previous_changes['weight1'] * previous_changes['weight2']
+            temp_target = ((previous_changes['value'] * previous_changes['weight']).sum()
+                           / previous_changes['weight'].sum())
+            new_targets.update({item: [temp_target - half_extent, temp_target + half_extent]})
+
+        return new_targets
+
+
 class TemporaryTargetBuilder:
     """
     Generate global target and format it for sending to TransferServer.
@@ -154,6 +188,27 @@ class TemporaryTargetBuilder:
         return {time_index: [self.lower_target, self.upper_target]}
 
 
+class Carrier(metaclass=ABCMeta):
+    """
+    Fetch all you need data by one http client.
+
+    """
+
+    @abstractmethod
+    async def fetch_all(self) -> None:
+        raise NotImplementedError
+
+
+class Packer(metaclass=ABCMeta):
+    """
+    Arrange raw data for using.
+    """
+
+    @abstractmethod
+    def run(self) -> Dict:
+        raise NotImplementedError
+
+
 class AdjustmentController(metaclass=ABCMeta):
     """
     Fetch some data, assemble target adjustment related functions and classes, send the new target to transfer server,
@@ -165,96 +220,258 @@ class AdjustmentController(metaclass=ABCMeta):
         raise NotImplementedError
 
 
-class TemperatureAdjustmentController(AdjustmentController):
+class TemperatureTargetCarrier(Carrier):
     """
-    Test strategy for Sequoia.
+    Fetch all the data that temperature target adjustment will use.
     """
 
-    def __init__(
-            self,
-            project_id: str,
-            space_id: str,
-            feedback: Optional[FeedbackValue] = None
-    ):
+    def __init__(self, project_id: str, object_id: str):
         self.project_id = project_id
-        self.space_id = space_id
-        self.feedback = feedback
+        self.object_id = object_id
+        self.result = {}
 
-    @logger.catch()
-    async def run(self) -> bool:
-        # Fetch all data from data platform and transfer server.
+    async def fetch_all(self) -> None:
         async with AsyncClient() as client:
-            transfer = SpaceInfoService(client, self.project_id, self.space_id)
+            transfer = SpaceInfoService(client, self.project_id, self.object_id)
             platform = DataPlatformService(client, self.project_id)
 
-            realtime_temperature = await platform.get_realtime_temperature(self.space_id)
-            logger.debug(f'realtime temperature: {realtime_temperature}')
+            realtime_temperature = await platform.get_realtime_temperature(self.object_id)
             all_day_targets = await transfer.get_custom_target()
             current_target = await transfer.get_current_temperature_target()
-            logger.debug(f'current target: {current_target}')
             is_customized = await transfer.is_customized()
-            logger.debug(f'is customized? {is_customized}')
             is_temporary = await transfer.is_temporary()
-            logger.debug(f'is temporary? {is_temporary}')
             season = await transfer.get_season()
 
-        # Arrange data
+        self.result = {
+            'realtime_temperature': realtime_temperature,
+            'all_day_targets': all_day_targets,
+            'current_target': current_target,
+            'is_customized': is_customized,
+            'is_temporary': is_temporary,
+            'season': season
+        }
+
+    async def get_result(self) -> Dict:
+        await self.fetch_all()
+        return self.result
+
+
+class TemperatureTargetV2Carrier(TemperatureTargetCarrier):
+    """
+    Add previous adjustment result to result.
+    """
+
+    async def fetch_previous_changes(self) -> None:
+        async with AsyncClient() as client:
+            transfer = SpaceInfoService(client, self.project_id, self.object_id)
+            previous_changes = await transfer.env_database_get()
+
+        self.result.update({'previous_changes': previous_changes['temperature']})
+
+    async def get_result(self) -> Dict:
+        await self.fetch_all()
+        await self.fetch_previous_changes()
+        return self.result
+
+
+class TemperatureTargetPacker:
+    """
+    Arrange raw data for temperature target adjustment.
+    """
+
+    def __init__(self, data):
+        self.result = data
+
+    def get_temperature_target(self):
+        all_day_targets = self.result['all_day_targets']
         if len(all_day_targets) > 0:
             extent = all_day_targets['temperatureMax'].iloc[0] - all_day_targets['temperatureMin'].iloc[0]
         else:
             extent = 2.0
-
-        temperature_all_day_targets = all_day_targets[['temperatureMin', 'temperatureMax']].copy()
+        temperature_all_day_targets = all_day_targets[['temperatureMin', 'temperatureMax']].copy().to_dict()
         target_params = {
-            'is_customized': is_customized,
-            'is_temporary': is_temporary,
-            'target_schedule': temperature_all_day_targets.to_dict(),
+            'is_customized': self.result['is_customized'],
+            'is_temporary': self.result['is_temporary'],
+            'target_schedule': temperature_all_day_targets,
             'extent': extent
         }
         target = TemperatureTarget(**target_params)
+        self.result.update({'target': target})
 
-        # Primary flow
+    def get_result(self) -> Dict:
+        self.get_temperature_target()
+        return self.result
+
+
+class TargetDeliver:
+    """
+    Send target adjustment result to transfer.
+    """
+
+    def __init__(self, project_id: str, space_id: str):
+        self.project_id = project_id
+        self.space_id = space_id
+
+    async def send(self, controlled_result: Dict):
+        async with AsyncClient() as client:
+            transfer = SpaceInfoService(client, self.project_id, self.space_id)
+            if controlled_result['need_switch_off']:
+                await transfer.set_temporary_custom()
+            if controlled_result['new_temporary_target']:
+                await transfer.set_custom_target('temperature', controlled_result['new_temporary_target'], '0')
+            if controlled_result['new_global_target']:
+                await transfer.set_custom_target('temperature', controlled_result['new_global_target'], '1')
+            if controlled_result['new_actual_target'] > 0 and controlled_result['need_run_room_control']:
+                await transfer.env_database_set('temperature', controlled_result['new_actual_target'])
+
+
+class TemperatureTargetController:
+    """
+    Primary flow of temperature target adjustment for Sequoia.
+    """
+
+    def __init__(self, data: Dict):
+        self.data = data
+        self.result = {}
+
+    def run(self, feedback: FeedbackValue):
         need_switch_off = False
         new_temporary_target = {}
         new_global_target = {}
         new_actual_target = 0
-        if self.feedback == FeedbackValue.switch_off:
+        if feedback == FeedbackValue.switch_off:
             need_switch_off = True
             need_run_room_control = True
-        elif self.feedback == FeedbackValue.switch_on:
+        elif feedback == FeedbackValue.switch_on:
             need_run_room_control = True
-            if not is_customized:
-                new_lower, new_upper = TemporaryTargetInit(1, 24).build(extent, season, realtime_temperature)
+            if not self.data['is_customized']:
+                new_lower, new_upper = TemporaryTargetInit(1, 24).build(
+                    self.data['extent'],
+                    self.data['season'],
+                    self.data['realtime_temperature']
+                )
                 new_temporary_target = TemporaryTargetBuilder(new_lower, new_upper)
-        elif (self.feedback == FeedbackValue.a_little_hot
-              or self.feedback == FeedbackValue.a_little_cold
-              or self.feedback == FeedbackValue.so_hot
-              or self.feedback == FeedbackValue.so_cold):
-            step_size = StepSizeCalculator(TEMPERATURE_TARGET_WEIGHT).run(realtime_temperature, 25.0, self.feedback)
+        elif (feedback == FeedbackValue.a_little_hot
+              or feedback == FeedbackValue.a_little_cold
+              or feedback == FeedbackValue.so_hot
+              or feedback == FeedbackValue.so_cold):
+            step_size = StepSizeCalculator(TEMPERATURE_TARGET_WEIGHT).run(
+                self.data['realtime_temperature'],
+                25.0,
+                feedback
+            )
             logger.debug(f'step size: {step_size}')
-            new_actual_target = NewTemperatureTargetBuilder(realtime_temperature, current_target, step_size).build()
-            if new_actual_target != current_target:
-                new_global_target = SimpleGlobalTemperatureTargetBuilder(target).build(new_actual_target)
+            new_actual_target = NewTemperatureTargetBuilder(
+                self.data['realtime_temperature'],
+                self.data['current_target'],
+                step_size
+            ).build()
+            if new_actual_target != self.data['current_target']:
+                new_global_target = SimpleGlobalTemperatureTargetBuilder(self.data['target']).build(new_actual_target)
                 need_run_room_control = True
             else:
                 need_run_room_control = False
         else:
             need_run_room_control = False
 
-        # Send adjustment result to transfer server
-        async with AsyncClient() as client:
-            transfer = SpaceInfoService(client, self.project_id, self.space_id)
-            if need_switch_off:
-                await transfer.set_temporary_custom()
-                logger.debug(f'turn off')
-            if new_temporary_target:
-                await transfer.set_custom_target('temperature', new_temporary_target, '0')
-                logger.debug(f'new temporary targets: {new_temporary_target}')
-            if new_global_target:
-                await transfer.set_custom_target('temperature', new_global_target, '1')
-                logger.debug(f'new global targets: {new_global_target}')
-            if new_actual_target != current_target:
-                await transfer.env_database_set('temperature', new_actual_target)
-                logger.debug(f'new actual target: {new_actual_target}')
-
-        return need_run_room_control
+        self.result.update({
+            'need_switch_off': need_switch_off,
+            'new_temporary_target': new_temporary_target,
+            'new_global_target': new_global_target,
+            'new_actual_target': new_actual_target,
+            'need_run_room_control': need_run_room_control
+        })
+
+    def get_result(self) -> Dict:
+        return self.result
+
+
+class TemperatureTargetControllerV2:
+    """
+    Primary flow of temperature target adjustment for Zhonghai.
+    """
+
+    def __init__(self, data: Dict):
+        self.data = data
+        self.result = {}
+
+    def run(self, feedback: FeedbackValue):
+        need_switch_off = False
+        new_temporary_target = {}
+        new_global_target = {}
+        new_actual_target = 0
+        if feedback == FeedbackValue.switch_off:
+            need_switch_off = True
+            need_run_room_control = True
+        elif feedback == FeedbackValue.switch_on:
+            need_run_room_control = True
+            if not self.data['is_customized']:
+                new_lower, new_upper = TemporaryTargetInit(1, 24).build(
+                    self.data['extent'],
+                    self.data['season'],
+                    self.data['realtime_temperature']
+                )
+                new_temporary_target = TemporaryTargetBuilder(new_lower, new_upper)
+        elif (feedback == FeedbackValue.a_little_hot
+              or feedback == FeedbackValue.a_little_cold
+              or feedback == FeedbackValue.so_hot
+              or feedback == FeedbackValue.so_cold):
+            step_size = StepSizeCalculator(TEMPERATURE_TARGET_WEIGHT).run(
+                self.data['realtime_temperature'],
+                25.0,
+                feedback
+            )
+            logger.debug(f'step size: {step_size}')
+            logger.debug(f'realtime: {self.data["realtime_temperature"]}')
+            logger.debug(f'current target: {self.data["current_target"]}')
+            new_actual_target = NewTemperatureTargetBuilder(
+                self.data['realtime_temperature'],
+                self.data['current_target'],
+                step_size
+            ).build()
+            if new_actual_target != self.data['current_target']:
+                new_global_target = ExpSmoothingTemperatureTargetBuilder(
+                    self.data['target'],
+                    self.data['previous_changes']
+                ).build(new_actual_target)
+                logger.debug(f'new global target: {new_global_target}')
+                need_run_room_control = True
+            else:
+                need_run_room_control = False
+        else:
+            need_run_room_control = False
+
+        self.result.update({
+            'need_switch_off': need_switch_off,
+            'new_temporary_target': new_temporary_target,
+            'new_global_target': new_global_target,
+            'new_actual_target': new_actual_target,
+            'need_run_room_control': need_run_room_control
+        })
+
+    def get_result(self) -> Dict:
+        return self.result
+
+
+@logger.catch()
+async def temperature_target_control_v1(project_id: str, space_id: str, feedback: FeedbackValue) -> bool:
+    temperature_target_raw_data = await TemperatureTargetCarrier(project_id, space_id).get_result()
+    temperature_target_data = TemperatureTargetPacker(temperature_target_raw_data).get_result()
+    controller = TemperatureTargetController(temperature_target_data)
+    controller.run(feedback)
+    controlled_result = controller.get_result()
+    await TargetDeliver(project_id, space_id).send(controlled_result)
+
+    return controlled_result['need_run_room_control']
+
+
+@logger.catch()
+async def temperature_target_control_v2(project_id: str, space_id: str, feedback: FeedbackValue) -> bool:
+    temperature_target_raw_data = await TemperatureTargetV2Carrier(project_id, space_id).get_result()
+    temperature_target_data = TemperatureTargetPacker(temperature_target_raw_data).get_result()
+    controller = TemperatureTargetControllerV2(temperature_target_data)
+    controller.run(feedback)
+    controlled_result = controller.get_result()
+    await TargetDeliver(project_id, space_id).send(controlled_result)
+
+    return controlled_result['need_run_room_control']