Browse Source

refactor temperature readjustment for Zhonghai

chenhaiyang 4 years ago
parent
commit
128c2d1e97
2 changed files with 282 additions and 67 deletions
  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 loguru import logger
 
 
 from app.controllers.targets.target import readjust_all_target
 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.feedback import FeedbackValue
 from app.models.domain.targets import TargetReadjustResponse
 from app.models.domain.targets import TargetReadjustResponse
 from app.utils.date import get_time_str
 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'),
         timestamp: Optional[str] = Query(None, min_length=14, max_length=14, alias='time'),
         feedback: Optional[FeedbackValue] = Query(None, alias='itemId')
         feedback: Optional[FeedbackValue] = Query(None, alias='itemId')
 ):
 ):
+    if not timestamp:
+        timestamp = get_time_str()
     try:
     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:
             else:
-                need_run_room_control = True
+                need_run_room_control = await temperature_target_control_v2(project_id, space_id, feedback)
         else:
         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:
     except Exception as e:
         logger.error(e)
         logger.error(e)
         raise HTTPException(
         raise HTTPException(
@@ -86,8 +85,7 @@ async def readjust_target_v2(
 ):
 ):
     if feedback != FeedbackValue.null:
     if feedback != FeedbackValue.null:
         try:
         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:
         except Exception as e:
             logger.error(e)
             logger.error(e)
             raise HTTPException(
             raise HTTPException(

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

@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 
 
 from abc import ABCMeta, abstractmethod
 from abc import ABCMeta, abstractmethod
-from typing import Dict, Optional, Tuple
+from typing import Dict, Tuple
 
 
 import arrow
 import arrow
 import numpy as np
 import numpy as np
+import pandas as pd
 from httpx import AsyncClient
 from httpx import AsyncClient
 from loguru import logger
 from loguru import logger
 
 
@@ -138,6 +139,39 @@ class SimpleGlobalTemperatureTargetBuilder(GlobalTargetBaseBuilder):
         return result
         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:
 class TemporaryTargetBuilder:
     """
     """
     Generate global target and format it for sending to TransferServer.
     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]}
         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):
 class AdjustmentController(metaclass=ABCMeta):
     """
     """
     Fetch some data, assemble target adjustment related functions and classes, send the new target to transfer server,
     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
         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.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:
         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)
             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()
             all_day_targets = await transfer.get_custom_target()
             current_target = await transfer.get_current_temperature_target()
             current_target = await transfer.get_current_temperature_target()
-            logger.debug(f'current target: {current_target}')
             is_customized = await transfer.is_customized()
             is_customized = await transfer.is_customized()
-            logger.debug(f'is customized? {is_customized}')
             is_temporary = await transfer.is_temporary()
             is_temporary = await transfer.is_temporary()
-            logger.debug(f'is temporary? {is_temporary}')
             season = await transfer.get_season()
             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:
         if len(all_day_targets) > 0:
             extent = all_day_targets['temperatureMax'].iloc[0] - all_day_targets['temperatureMin'].iloc[0]
             extent = all_day_targets['temperatureMax'].iloc[0] - all_day_targets['temperatureMin'].iloc[0]
         else:
         else:
             extent = 2.0
             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 = {
         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
             'extent': extent
         }
         }
         target = TemperatureTarget(**target_params)
         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
         need_switch_off = False
         new_temporary_target = {}
         new_temporary_target = {}
         new_global_target = {}
         new_global_target = {}
         new_actual_target = 0
         new_actual_target = 0
-        if self.feedback == FeedbackValue.switch_off:
+        if feedback == FeedbackValue.switch_off:
             need_switch_off = True
             need_switch_off = True
             need_run_room_control = True
             need_run_room_control = True
-        elif self.feedback == FeedbackValue.switch_on:
+        elif feedback == FeedbackValue.switch_on:
             need_run_room_control = True
             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)
                 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}')
             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
                 need_run_room_control = True
             else:
             else:
                 need_run_room_control = False
                 need_run_room_control = False
         else:
         else:
             need_run_room_control = False
             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']