solax-py-library 1.0.0.2507__tar.gz → 1.0.0.2508__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/PKG-INFO +1 -1
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/pyproject.toml +1 -1
- solax_py_library-1.0.0.2508/solax_py_library/smart_scene/core/condition/__init__.py +17 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/condition/base.py +2 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/condition/cabinet_condition.py +3 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/condition/date_condition.py +3 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/condition/price_condition.py +5 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/condition/system_condition.py +3 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/condition/weather_condition.py +3 -0
- solax_py_library-1.0.0.2508/solax_py_library/smart_scene/core/service/__init__.py +3 -0
- solax_py_library-1.0.0.2508/solax_py_library/smart_scene/core/service/run.py +154 -0
- solax_py_library-1.0.0.2508/solax_py_library/smart_scene/exceptions/__init__.py +7 -0
- solax_py_library-1.0.0.2507/solax_py_library/smart_scene/core/service/smart_scene_service.py +0 -411
- solax_py_library-1.0.0.2507/solax_py_library/test/test_utils/__init__.py +0 -0
- solax_py_library-1.0.0.2507/solax_py_library/upload/test/__init__.py +0 -0
- solax_py_library-1.0.0.2507/solax_py_library/utils/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/README.md +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/constant/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/constant/cabinet.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/constant/inverter_model_info.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/core/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/core/interver/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/core/interver/base.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/types/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/types/alarm.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/types/inverter_config.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/types/modbus_point.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/exception.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/constant/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/constant/message_entry.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/action/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/action/base.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/action/ems_action.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/core/action/system_action.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/exceptions/price.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/exceptions/smart_scene.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/exceptions/weather.py +0 -0
- {solax_py_library-1.0.0.2507/solax_py_library/smart_scene/core/condition → solax_py_library-1.0.0.2508/solax_py_library/smart_scene/types}/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/types/action.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/types/condition.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/types/smart_scene_content.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/constant/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/constant/crc_table.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/core/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/core/base_modbus.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/core/parser.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/core/snap_shot.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/exceptions/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/exceptions/snap_shot.py +0 -0
- {solax_py_library-1.0.0.2507/solax_py_library/smart_scene/core/service → solax_py_library-1.0.0.2508/solax_py_library/snap_shot/test}/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/types/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/types/address.py +0 -0
- {solax_py_library-1.0.0.2507/solax_py_library/smart_scene/exceptions → solax_py_library-1.0.0.2508/solax_py_library/test}/__init__.py +0 -0
- {solax_py_library-1.0.0.2507/solax_py_library/smart_scene/types → solax_py_library-1.0.0.2508/solax_py_library/test/test_utils}/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/test/test_utils/test_cloud_client.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/api/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/api/service.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/core/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/core/data_adapter/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/core/data_adapter/base.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/core/data_adapter/csv.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/core/upload_service/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/core/upload_service/base.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/core/upload_service/ftp.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/exceptions/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/exceptions/upload_error.py +0 -0
- {solax_py_library-1.0.0.2507/solax_py_library/snap_shot → solax_py_library-1.0.0.2508/solax_py_library/upload}/test/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/test/test_ftp.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/types/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/types/client.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/types/ftp.py +0 -0
- {solax_py_library-1.0.0.2507/solax_py_library/test → solax_py_library-1.0.0.2508/solax_py_library/utils}/__init__.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/utils/cloud_client.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/utils/common.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/utils/struct_util.py +0 -0
- {solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/utils/time_util.py +0 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
from .base import BaseCondition
|
2
|
+
from .cabinet_condition import CabinetCondition
|
3
|
+
from .date_condition import DateCondition
|
4
|
+
from .weather_condition import WeatherCondition
|
5
|
+
from .price_condition import EleSellPriceCondition, ElsBuyPriceCondition
|
6
|
+
from .system_condition import SystemCondition
|
7
|
+
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"BaseCondition",
|
11
|
+
"CabinetCondition",
|
12
|
+
"DateCondition",
|
13
|
+
"WeatherCondition",
|
14
|
+
"ElsBuyPriceCondition",
|
15
|
+
"EleSellPriceCondition",
|
16
|
+
"SystemCondition",
|
17
|
+
]
|
@@ -4,11 +4,14 @@ from solax_py_library.smart_scene.core.condition.base import BaseCondition
|
|
4
4
|
from solax_py_library.smart_scene.types.condition import (
|
5
5
|
CabinetConditionItemData,
|
6
6
|
CabinetConditionType,
|
7
|
+
ConditionType,
|
7
8
|
)
|
8
9
|
from solax_py_library.device.types.alarm import AlarmLevel
|
9
10
|
|
10
11
|
|
11
12
|
class CabinetCondition(BaseCondition):
|
13
|
+
condition_type = ConditionType.cabinet
|
14
|
+
|
12
15
|
def __init__(self, update_value_function, **kwargs):
|
13
16
|
super().__init__(update_value_function, **kwargs)
|
14
17
|
self.value = defaultdict(
|
@@ -4,10 +4,13 @@ from solax_py_library.smart_scene.core.condition.base import BaseCondition
|
|
4
4
|
from solax_py_library.smart_scene.types.condition import (
|
5
5
|
DateConditionItemData,
|
6
6
|
DateConditionType,
|
7
|
+
ConditionType,
|
7
8
|
)
|
8
9
|
|
9
10
|
|
10
11
|
class DateCondition(BaseCondition):
|
12
|
+
condition_type = ConditionType.date
|
13
|
+
|
11
14
|
def __init__(self, update_value_function, **kwargs):
|
12
15
|
super().__init__(update_value_function, **kwargs)
|
13
16
|
|
@@ -6,6 +6,7 @@ from solax_py_library.smart_scene.types.condition import (
|
|
6
6
|
PriceConditionType,
|
7
7
|
SmartSceneUnit,
|
8
8
|
ConditionFunc,
|
9
|
+
ConditionType,
|
9
10
|
)
|
10
11
|
from solax_py_library.utils.time_util import (
|
11
12
|
trans_str_time_to_index,
|
@@ -88,12 +89,16 @@ class ElePriceCondition(BaseCondition):
|
|
88
89
|
|
89
90
|
|
90
91
|
class EleSellPriceCondition(ElePriceCondition):
|
92
|
+
condition_type = ConditionType.sellingPrice
|
93
|
+
|
91
94
|
def __init__(self, update_value_function, **kwargs):
|
92
95
|
super().__init__(update_value_function, **kwargs)
|
93
96
|
self.buy = False
|
94
97
|
|
95
98
|
|
96
99
|
class ElsBuyPriceCondition(ElePriceCondition):
|
100
|
+
condition_type = ConditionType.buyingPrice
|
101
|
+
|
97
102
|
def __init__(self, update_value_function, **kwargs):
|
98
103
|
super().__init__(update_value_function, **kwargs)
|
99
104
|
self.buy = True
|
@@ -2,10 +2,13 @@ from solax_py_library.smart_scene.core.condition.base import BaseCondition
|
|
2
2
|
from solax_py_library.smart_scene.types.condition import (
|
3
3
|
SystemConditionItemData,
|
4
4
|
SystemConditionType,
|
5
|
+
ConditionType,
|
5
6
|
)
|
6
7
|
|
7
8
|
|
8
9
|
class SystemCondition(BaseCondition):
|
10
|
+
condition_type = ConditionType.systemCondition
|
11
|
+
|
9
12
|
def __init__(self, update_value_function, **kwargs):
|
10
13
|
super().__init__(update_value_function, **kwargs)
|
11
14
|
self.grid_active_power = None
|
@@ -2,11 +2,14 @@ from solax_py_library.smart_scene.core.condition.base import BaseCondition
|
|
2
2
|
from solax_py_library.smart_scene.types.condition import (
|
3
3
|
WeatherConditionItemData,
|
4
4
|
WeatherConditionType,
|
5
|
+
ConditionType,
|
5
6
|
)
|
6
7
|
from solax_py_library.utils.time_util import get_rounded_times
|
7
8
|
|
8
9
|
|
9
10
|
class WeatherCondition(BaseCondition):
|
11
|
+
condition_type = ConditionType.weather
|
12
|
+
|
10
13
|
def __init__(self, update_value_function, **kwargs):
|
11
14
|
super().__init__(update_value_function, **kwargs)
|
12
15
|
|
@@ -0,0 +1,154 @@
|
|
1
|
+
import json
|
2
|
+
import traceback
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import List, Set
|
5
|
+
|
6
|
+
from solax_py_library.smart_scene.types.action import (
|
7
|
+
SmartSceneAction,
|
8
|
+
)
|
9
|
+
from solax_py_library.smart_scene.types.condition import (
|
10
|
+
ConditionType,
|
11
|
+
RepeatFunc,
|
12
|
+
LogicFunc,
|
13
|
+
SmartSceneCondition,
|
14
|
+
DateConditionType,
|
15
|
+
ConditionItem,
|
16
|
+
)
|
17
|
+
from solax_py_library.smart_scene.types.smart_scene_content import (
|
18
|
+
SmartSceneContent,
|
19
|
+
SmartSceneOtherInfo,
|
20
|
+
)
|
21
|
+
from tornado.log import app_log
|
22
|
+
|
23
|
+
|
24
|
+
class SmartSceneRunner:
|
25
|
+
def __init__(self, condition_class_map, action_class_map):
|
26
|
+
self.condition_class_map = condition_class_map
|
27
|
+
self.action_class_map = action_class_map
|
28
|
+
|
29
|
+
@staticmethod
|
30
|
+
def _handle_repeat_info(scene_content: SmartSceneContent, once_flag):
|
31
|
+
# once_flag: 是否执行过
|
32
|
+
if scene_content.repeatFunction == RepeatFunc.ONCE:
|
33
|
+
return False if once_flag else True
|
34
|
+
week_list = scene_content.get_weekday_index()
|
35
|
+
if datetime.now().weekday() + 1 in week_list:
|
36
|
+
return True
|
37
|
+
return False
|
38
|
+
|
39
|
+
def smart_scene_handle(self, scene_instance):
|
40
|
+
"""smart scene的核心,判断是否满足条件并执行"""
|
41
|
+
condition_types = set() # 记录需要判断的父条件类型,进行数据更新
|
42
|
+
# for scene in scene_list:
|
43
|
+
other_info = SmartSceneOtherInfo.parse_raw(scene_instance.other)
|
44
|
+
try:
|
45
|
+
scene_data = SmartSceneContent.parse_raw(scene_instance.content)
|
46
|
+
|
47
|
+
# repeat,是否执行
|
48
|
+
if not self._handle_repeat_info(scene_data, other_info.once_flag):
|
49
|
+
app_log.info(
|
50
|
+
f"场景名称: {scene_data.name}, 场景ID: {scene_instance.scene_id} 不符合重复条件,不执行"
|
51
|
+
)
|
52
|
+
return
|
53
|
+
scene_id = scene_instance.scene_id
|
54
|
+
app_log.info(
|
55
|
+
f"场景名称: {scene_data.name}, 场景ID: {scene_id}, 判断类型: {scene_data.conditions.operation}:"
|
56
|
+
)
|
57
|
+
|
58
|
+
# 更新
|
59
|
+
cur_condition_types = scene_data.get_condition_types() - condition_types
|
60
|
+
if cur_condition_types:
|
61
|
+
self._update_all_condition_data(cur_condition_types)
|
62
|
+
condition_types |= cur_condition_types
|
63
|
+
|
64
|
+
# 条件判断
|
65
|
+
result = self._handle_conditions(
|
66
|
+
scene_data.conditions.operation,
|
67
|
+
scene_data.conditions,
|
68
|
+
other_info,
|
69
|
+
)
|
70
|
+
|
71
|
+
# 动作执行
|
72
|
+
new_exec_number = scene_instance.exec_number
|
73
|
+
if result:
|
74
|
+
self._handle_action(scene_id, scene_data.thenActions, log_prefix="THEN")
|
75
|
+
new_exec_number = 1
|
76
|
+
other_info.once_flag = True
|
77
|
+
elif scene_instance.exec_number == 1 and scene_data.elseThenActions:
|
78
|
+
self._handle_action(
|
79
|
+
scene_id, scene_data.elseThenActions, log_prefix="ELSE THEN"
|
80
|
+
)
|
81
|
+
new_exec_number = 0
|
82
|
+
other_info.duration_times = 0
|
83
|
+
|
84
|
+
app_log.info(f"{scene_id} 执行完毕\n")
|
85
|
+
scene_instance.exec_number = new_exec_number
|
86
|
+
scene_instance.other = json.dumps(other_info.dict())
|
87
|
+
return scene_instance
|
88
|
+
except Exception:
|
89
|
+
app_log.error(
|
90
|
+
f"{scene_instance.scene_id} 执行智能场景异常 {traceback.format_exc()}"
|
91
|
+
)
|
92
|
+
|
93
|
+
def _handle_action(
|
94
|
+
self, scene_id, actions: List[SmartSceneAction], log_prefix="THEN"
|
95
|
+
):
|
96
|
+
if not actions:
|
97
|
+
return
|
98
|
+
for action_info in actions:
|
99
|
+
action_class = self.action_class_map[action_info.type]
|
100
|
+
for child_info in action_info.data:
|
101
|
+
ret = action_class.do_func(scene_id=scene_id, data=child_info)
|
102
|
+
app_log.info(f"{log_prefix}条件 {child_info} 执行结果: {ret}")
|
103
|
+
|
104
|
+
def _handle_conditions(
|
105
|
+
self,
|
106
|
+
operation: LogicFunc,
|
107
|
+
conditions: SmartSceneCondition,
|
108
|
+
other_info: SmartSceneOtherInfo,
|
109
|
+
):
|
110
|
+
need_duration_times = conditions.get_duration_info()
|
111
|
+
if not self._check_conditions(operation, conditions):
|
112
|
+
other_info.duration_times = 0
|
113
|
+
return False
|
114
|
+
if other_info.duration_times < need_duration_times:
|
115
|
+
other_info.duration_times += 1
|
116
|
+
app_log.info(
|
117
|
+
f"need times: {need_duration_times}, current times: {other_info.duration_times}"
|
118
|
+
)
|
119
|
+
return other_info.duration_times >= need_duration_times
|
120
|
+
|
121
|
+
def _check_conditions(self, operation, conditions):
|
122
|
+
ret_list = []
|
123
|
+
for cond in conditions.value:
|
124
|
+
parent_type = cond.type # 父条件类型 (日期、天气、电价)
|
125
|
+
condition_class = self.condition_class_map[parent_type]
|
126
|
+
ctx = self._build_condition_ctx(cond)
|
127
|
+
for child_info in cond.data:
|
128
|
+
if child_info.childType == DateConditionType.duration:
|
129
|
+
continue
|
130
|
+
ret = condition_class.meet_func(data=child_info, ctx=ctx)
|
131
|
+
app_log.info(f"IF条件 {child_info} 判断结果: {ret}")
|
132
|
+
ret_list.append(ret)
|
133
|
+
# 如果该条件判断不满足,并且客户设置为and,则无需继续判断,返回失败
|
134
|
+
if not ret and operation == LogicFunc.AND:
|
135
|
+
return False
|
136
|
+
# 如果条件判断满足,并且客户设置为or,则无需继续判断,返回成功
|
137
|
+
if ret and operation == LogicFunc.OR:
|
138
|
+
return True
|
139
|
+
if (operation == LogicFunc.AND and all(ret_list)) or (
|
140
|
+
operation == LogicFunc.OR and any(ret_list)
|
141
|
+
):
|
142
|
+
return True
|
143
|
+
return False
|
144
|
+
|
145
|
+
@staticmethod
|
146
|
+
def _build_condition_ctx(condition_info: ConditionItem):
|
147
|
+
ctx = {}
|
148
|
+
if condition_info.type == ConditionType.cabinet:
|
149
|
+
ctx["cabinet"] = condition_info.cabinet
|
150
|
+
return ctx
|
151
|
+
|
152
|
+
def _update_all_condition_data(self, condition_types: Set[ConditionType]):
|
153
|
+
for cond_type in condition_types:
|
154
|
+
self.condition_class_map[cond_type].update_value()
|
solax_py_library-1.0.0.2507/solax_py_library/smart_scene/core/service/smart_scene_service.py
DELETED
@@ -1,411 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import os.path
|
3
|
-
import struct
|
4
|
-
import traceback
|
5
|
-
import threading
|
6
|
-
import subprocess as sp
|
7
|
-
|
8
|
-
import requests
|
9
|
-
from datetime import datetime, timedelta
|
10
|
-
|
11
|
-
from tornado.log import app_log
|
12
|
-
|
13
|
-
from domain.ems_enum.smart_scene.condition import ConditionFunc
|
14
|
-
from settings.const import (
|
15
|
-
RedisKey,
|
16
|
-
Role,
|
17
|
-
DeviceInfo,
|
18
|
-
PCSOperateFunctionName,
|
19
|
-
Enumeration,
|
20
|
-
FilePath,
|
21
|
-
)
|
22
|
-
from web.web_ext import config_json
|
23
|
-
from models.tables import Device
|
24
|
-
from utils.common import options, get_logger_sn, md5_encrypt, read_write_json_file
|
25
|
-
from services.redis_service import RedisService
|
26
|
-
from models.sqlite_db import session_maker
|
27
|
-
from utils.redis_utils import RedisProducer
|
28
|
-
from services.strategy_control_service import StrategyControlService
|
29
|
-
from solax_py_library.utils.cloud_client import CloudClient
|
30
|
-
|
31
|
-
redis_host = options.REDIS_HOST
|
32
|
-
redis_port = options.REDIS_PORT
|
33
|
-
db = options.REDIS_DB
|
34
|
-
LOCAL_URL = "http://127.0.0.1:8000/ems/cloud/"
|
35
|
-
|
36
|
-
|
37
|
-
class SmartSceneService(object):
|
38
|
-
def __init__(self):
|
39
|
-
self.ems_sn = get_logger_sn()
|
40
|
-
self.redis_service = RedisService()
|
41
|
-
self.redis_producer = RedisProducer().producer
|
42
|
-
self.strategy_control_service = StrategyControlService()
|
43
|
-
|
44
|
-
def request_local_redis(self, redis_topic, payload):
|
45
|
-
"""本地redis分发任务"""
|
46
|
-
try:
|
47
|
-
self.redis_producer.publish(redis_topic, payload)
|
48
|
-
except Exception:
|
49
|
-
app_log.info(traceback.format_exc())
|
50
|
-
|
51
|
-
def request_local_server(self, apiName, apiPostData, scene_id=None):
|
52
|
-
"""
|
53
|
-
访问本地接口, 调用server8000
|
54
|
-
需要使用admin权限
|
55
|
-
:param apiName: 接口名称
|
56
|
-
:param apiPostData: 请求参数
|
57
|
-
:param scene_id: 请求标识
|
58
|
-
:return:
|
59
|
-
"""
|
60
|
-
username = Role.SUPER_ADMIN
|
61
|
-
user_info = {"username": username, "password": md5_encrypt(self.ems_sn)}
|
62
|
-
token = self.redis_service.make_token(username, user_info)
|
63
|
-
url = LOCAL_URL + apiName
|
64
|
-
headers = {"token": token}
|
65
|
-
try:
|
66
|
-
response = requests.post(url, json=apiPostData, headers=headers, timeout=5)
|
67
|
-
app_log.info(
|
68
|
-
f"scene_id: {scene_id}, request apiName: {apiName}, response: {response.text}"
|
69
|
-
)
|
70
|
-
return json.loads(response.text)
|
71
|
-
except Exception:
|
72
|
-
app_log.info(traceback.format_exc())
|
73
|
-
return False
|
74
|
-
|
75
|
-
def get_token(self):
|
76
|
-
token = self.redis_service.get_token()
|
77
|
-
if not token:
|
78
|
-
client = CloudClient(base_url=config_json.get("CLOUD_URL", ""))
|
79
|
-
token = client.get_token(
|
80
|
-
ems_sn=self.ems_sn, sn_secret=config_json.get("MQTT_PASSWORD")
|
81
|
-
)
|
82
|
-
if not token:
|
83
|
-
app_log.info("访问token接口失败")
|
84
|
-
return False
|
85
|
-
self.redis_service.set_token(token)
|
86
|
-
return token
|
87
|
-
else:
|
88
|
-
return token
|
89
|
-
|
90
|
-
def get_weather_data_from_redis(self):
|
91
|
-
try:
|
92
|
-
weather_info = self.redis_service.get_ele_price_info() or {}
|
93
|
-
return weather_info
|
94
|
-
except Exception:
|
95
|
-
app_log.error(traceback.format_exc())
|
96
|
-
return {}
|
97
|
-
|
98
|
-
def get_weather_data_from_cloud(self):
|
99
|
-
"""获取未来24小时天气数据"""
|
100
|
-
try:
|
101
|
-
token = self.get_token()
|
102
|
-
client = CloudClient(base_url=config_json.get("CLOUD_URL", ""))
|
103
|
-
weather_info = client.get_weather_data_from_cloud(
|
104
|
-
ems_sn=self.ems_sn,
|
105
|
-
token=token,
|
106
|
-
)
|
107
|
-
if not weather_info:
|
108
|
-
app_log.error(f"获取天气数据失败 异常 {traceback.format_exc()}")
|
109
|
-
return False
|
110
|
-
app_log.info(f"获取天气数据成功: {weather_info}")
|
111
|
-
self.redis_service.set_weather_info(weather_info)
|
112
|
-
except Exception:
|
113
|
-
app_log.error(f"获取天气数据失败 异常 {traceback.format_exc()}")
|
114
|
-
return False
|
115
|
-
|
116
|
-
def trans_str_time_to_index(self, now_time, minute=15):
|
117
|
-
"""将时间按照minute切换为索引,时间格式为 %H-%M"""
|
118
|
-
time_list = [int(i) for i in now_time.split(":")]
|
119
|
-
time_int = time_list[0] * 4 + time_list[1] // minute
|
120
|
-
return time_int
|
121
|
-
|
122
|
-
def get_electrovalence_data_from_cloud(self):
|
123
|
-
try:
|
124
|
-
token = self.get_token()
|
125
|
-
client = CloudClient(base_url=config_json.get("CLOUD_URL", ""))
|
126
|
-
ele_price_info = client.get_electrovalence_data_from_cloud(
|
127
|
-
ems_sn=self.ems_sn, token=token
|
128
|
-
)
|
129
|
-
app_log.info(f"获取电价数据成功: {ele_price_info}")
|
130
|
-
self.redis_service.set_ele_price_info(ele_price_info)
|
131
|
-
except Exception:
|
132
|
-
app_log.error(f"获取电价数据失败 异常 {traceback.format_exc()}")
|
133
|
-
return False
|
134
|
-
|
135
|
-
def get_electrovalence_data_from_redis(self):
|
136
|
-
"""从自定义电价的缓存中获取"""
|
137
|
-
month_temp = self.redis_service.hget(
|
138
|
-
RedisKey.MONTH_TEMPLATE, RedisKey.MONTH_TEMPLATE
|
139
|
-
)
|
140
|
-
month_temp = json.loads(month_temp) if month_temp else {}
|
141
|
-
if month_temp == {}:
|
142
|
-
return {}
|
143
|
-
month = datetime.strftime(datetime.now(), "%m")
|
144
|
-
month_map = {
|
145
|
-
"01": "Jan",
|
146
|
-
"02": "Feb",
|
147
|
-
"03": "Mar",
|
148
|
-
"04": "Apr",
|
149
|
-
"05": "May",
|
150
|
-
"06": "Jun",
|
151
|
-
"07": "Jul",
|
152
|
-
"08": "Aug",
|
153
|
-
"09": "Sep",
|
154
|
-
"10": "Oct",
|
155
|
-
"11": "Nov",
|
156
|
-
"12": "Dec",
|
157
|
-
}
|
158
|
-
month = month_map.get(month, None)
|
159
|
-
if month is None:
|
160
|
-
return {}
|
161
|
-
template_id = str(month_temp.get(month, 0))
|
162
|
-
price_info = self.redis_service.hget(
|
163
|
-
RedisKey.ELECTRICITY_PRICE_TEMPLATE, template_id
|
164
|
-
)
|
165
|
-
price_info = json.loads(price_info) if price_info else {}
|
166
|
-
if price_info == {}:
|
167
|
-
return {}
|
168
|
-
|
169
|
-
stationInfo = self.redis_service.get(RedisKey.STATION_INFO)
|
170
|
-
stationInfo = json.loads(stationInfo) if stationInfo else {}
|
171
|
-
currencyCode = stationInfo.get("currencyCode")
|
172
|
-
currency_file = read_write_json_file(FilePath.CURRENCY_PATH)
|
173
|
-
ele_unit = False
|
174
|
-
try:
|
175
|
-
for j in currency_file["list"]:
|
176
|
-
if currencyCode == j["code"]:
|
177
|
-
ele_unit = j["unit"].split(":")[-1] + "/kWh"
|
178
|
-
break
|
179
|
-
except:
|
180
|
-
app_log.error(f"获取本地电价单位出错 {traceback.format_exc()}")
|
181
|
-
ele_price_info = {
|
182
|
-
"buy": [None] * 192,
|
183
|
-
"sell": [None] * 192,
|
184
|
-
"date": datetime.strftime(datetime.now(), "%Y-%m-%d"),
|
185
|
-
"ele_unit": ele_unit if ele_unit else "/kWh",
|
186
|
-
}
|
187
|
-
for period in price_info["periodConfiguration"]:
|
188
|
-
start_index = self.trans_str_time_to_index(period["startTime"])
|
189
|
-
end_index = self.trans_str_time_to_index(period["endTime"])
|
190
|
-
slotName = period["slotName"]
|
191
|
-
for price in price_info["priceAllocation"]:
|
192
|
-
if price["slotName"] == slotName:
|
193
|
-
ele_price_info["buy"][start_index:end_index] = [
|
194
|
-
price["buyPrice"]
|
195
|
-
] * (end_index - start_index)
|
196
|
-
ele_price_info["sell"][start_index:end_index] = [
|
197
|
-
price["salePrice"]
|
198
|
-
] * (end_index - start_index)
|
199
|
-
break
|
200
|
-
return ele_price_info
|
201
|
-
|
202
|
-
def get_electrovalence_data(self):
|
203
|
-
try:
|
204
|
-
online_status = self.redis_service.hget(RedisKey.LED_STATUS_KEY, "blue")
|
205
|
-
online_status = (
|
206
|
-
json.loads(online_status).get("status") if online_status else 0
|
207
|
-
)
|
208
|
-
if online_status:
|
209
|
-
ele_price_info = self.redis_service.get_ele_price_info() or {}
|
210
|
-
today = datetime.strftime(datetime.now(), "%Y-%m-%d")
|
211
|
-
date_now = ele_price_info.get("date", "")
|
212
|
-
if today not in date_now:
|
213
|
-
ele_price_info = {}
|
214
|
-
else:
|
215
|
-
app_log.info("设备离线,获取本地电价")
|
216
|
-
ele_price_info = self.get_electrovalence_data_from_redis()
|
217
|
-
return ele_price_info
|
218
|
-
except Exception:
|
219
|
-
app_log.error(traceback.format_exc())
|
220
|
-
return {}
|
221
|
-
|
222
|
-
def get_highest_or_lowest_price(
|
223
|
-
self, start_time, end_time, hours, price_list, func="expensive_hours"
|
224
|
-
):
|
225
|
-
"""获取一段时间内,电价最高或最低的几个小时"""
|
226
|
-
start_index = self.trans_str_time_to_index(start_time)
|
227
|
-
end_index = self.trans_str_time_to_index(end_time)
|
228
|
-
arr = price_list[start_index:end_index]
|
229
|
-
if None in arr:
|
230
|
-
return False
|
231
|
-
indices = list(range(end_index - start_index))
|
232
|
-
if func == "expensive_hours":
|
233
|
-
reverse = True
|
234
|
-
else:
|
235
|
-
reverse = False
|
236
|
-
sorted_indices = sorted(indices, key=lambda i: arr[i], reverse=reverse)
|
237
|
-
return sorted_indices[: int(hours * 4)], start_index
|
238
|
-
|
239
|
-
def get_rounded_times(self):
|
240
|
-
"""
|
241
|
-
返回距离当前时间最近的15min的整点时间以及后一整点5min时间(天气是预测未来15min的,也就是在00:00时,只能拿到00:15的数据)
|
242
|
-
"""
|
243
|
-
now = datetime.now()
|
244
|
-
# 确定当前时间所属的15分钟区间
|
245
|
-
index_1 = now.minute // 15
|
246
|
-
index_2 = now.minute % 15
|
247
|
-
left_time = now.replace(minute=15 * index_1, second=0, microsecond=0)
|
248
|
-
right_time = left_time + timedelta(minutes=15)
|
249
|
-
if index_2 < 8:
|
250
|
-
nearest_time = left_time
|
251
|
-
else:
|
252
|
-
nearest_time = right_time
|
253
|
-
return datetime.strftime(nearest_time, "%Y-%m-%d %H:%M:%S"), datetime.strftime(
|
254
|
-
right_time, "%Y-%m-%d %H:%M:%S"
|
255
|
-
)
|
256
|
-
|
257
|
-
def compare_the_magnitudes(self, function, compare_value, base_value):
|
258
|
-
"""比较两个值"""
|
259
|
-
if function == ConditionFunc.GT:
|
260
|
-
return compare_value > base_value
|
261
|
-
elif function == ConditionFunc.EQ:
|
262
|
-
return compare_value == base_value
|
263
|
-
elif function == ConditionFunc.LT:
|
264
|
-
return compare_value < base_value
|
265
|
-
return False
|
266
|
-
|
267
|
-
def get_cabinet_type(self):
|
268
|
-
# 获取机柜类型
|
269
|
-
cabinet_type = 1
|
270
|
-
with session_maker(change=False) as session:
|
271
|
-
result = (
|
272
|
-
session.query(Device.deviceModel)
|
273
|
-
.filter(Device.deviceType == DeviceInfo.ESS_TYPE, Device.isDelete == 0)
|
274
|
-
.first()
|
275
|
-
)
|
276
|
-
|
277
|
-
if result:
|
278
|
-
cabinet_type = result[0]
|
279
|
-
if cabinet_type in [3, 4, 7, 8]:
|
280
|
-
key_name = "AELIO"
|
281
|
-
else:
|
282
|
-
key_name = "TRENE"
|
283
|
-
return key_name
|
284
|
-
|
285
|
-
def set_manual_mode(self, work_mode, power, soc, cabinet_type):
|
286
|
-
"""设置手动模式"""
|
287
|
-
# 充电
|
288
|
-
if work_mode == 3:
|
289
|
-
strategy_info = {
|
290
|
-
"chargePower": power,
|
291
|
-
"chargeTargetSoc": soc,
|
292
|
-
"runState": 1,
|
293
|
-
}
|
294
|
-
self.redis_service.hset(
|
295
|
-
RedisKey.MANUAL_STRATEGY_INFO,
|
296
|
-
RedisKey.CHARGE_MODE,
|
297
|
-
json.dumps(strategy_info),
|
298
|
-
)
|
299
|
-
self.strategy_control_service.apply_or_stop_strategy(
|
300
|
-
0, RedisKey.MANUAL_STRATEGY_INFO, RedisKey.DISCHARGE_MODE
|
301
|
-
)
|
302
|
-
# 将运行模式修改为手动
|
303
|
-
self.redis_service.set(RedisKey.RUN_STRATEGY_TYPE, 1)
|
304
|
-
elif work_mode == 4:
|
305
|
-
strategy_info = {
|
306
|
-
"dischargePower": power,
|
307
|
-
"dischargeTargetSoc": soc,
|
308
|
-
"runState": 1,
|
309
|
-
}
|
310
|
-
self.redis_service.hset(
|
311
|
-
RedisKey.MANUAL_STRATEGY_INFO,
|
312
|
-
RedisKey.DISCHARGE_MODE,
|
313
|
-
json.dumps(strategy_info),
|
314
|
-
)
|
315
|
-
# 将充电状态修改
|
316
|
-
self.strategy_control_service.apply_or_stop_strategy(
|
317
|
-
0, RedisKey.MANUAL_STRATEGY_INFO, RedisKey.CHARGE_MODE
|
318
|
-
)
|
319
|
-
# 将运行模式修改为手动
|
320
|
-
self.redis_service.set(RedisKey.RUN_STRATEGY_TYPE, 1)
|
321
|
-
# 停止
|
322
|
-
else:
|
323
|
-
self.strategy_control_service.apply_or_stop_strategy(
|
324
|
-
0, RedisKey.MANUAL_STRATEGY_INFO, RedisKey.DISCHARGE_MODE
|
325
|
-
)
|
326
|
-
self.strategy_control_service.apply_or_stop_strategy(
|
327
|
-
0, RedisKey.MANUAL_STRATEGY_INFO, RedisKey.CHARGE_MODE
|
328
|
-
)
|
329
|
-
self.redis_service.set(RedisKey.RUN_STRATEGY_TYPE, 1)
|
330
|
-
if work_mode == 3:
|
331
|
-
manualType = 1
|
332
|
-
elif work_mode == 4:
|
333
|
-
manualType = 2
|
334
|
-
else:
|
335
|
-
manualType = 3
|
336
|
-
data = {
|
337
|
-
"power": power,
|
338
|
-
"soc": soc,
|
339
|
-
"workMode": work_mode,
|
340
|
-
"manualType": manualType,
|
341
|
-
"useMode": 3,
|
342
|
-
}
|
343
|
-
if cabinet_type in ["TRENE"]:
|
344
|
-
t = threading.Thread(
|
345
|
-
target=self.request_local_redis, args=(options.REDIS_POWER_CONTROL, "")
|
346
|
-
)
|
347
|
-
else:
|
348
|
-
func_name = PCSOperateFunctionName.SET_AELIO_USE_MODE
|
349
|
-
channel = options.REDIS_WRITE_SERIAL_DEVICE
|
350
|
-
future_data = {}
|
351
|
-
future_data["func_name"] = func_name
|
352
|
-
future_data["operationMode"] = Enumeration.SINGLE_DEVICE_MODE
|
353
|
-
future_data["data"] = data
|
354
|
-
t = threading.Thread(
|
355
|
-
target=self.request_local_redis,
|
356
|
-
args=(channel, json.dumps(future_data)),
|
357
|
-
daemon=True,
|
358
|
-
)
|
359
|
-
t.start()
|
360
|
-
return True
|
361
|
-
|
362
|
-
def struct_transform(self, value, fmt, order="big"):
|
363
|
-
"""将10进制的原始值转换为modbus协议需要的精度与类型的值"""
|
364
|
-
value = int(value)
|
365
|
-
if order == "little":
|
366
|
-
opt = "<"
|
367
|
-
else:
|
368
|
-
opt = ">"
|
369
|
-
try:
|
370
|
-
if fmt == "int16":
|
371
|
-
ret = struct.pack(f"{opt}h", value)
|
372
|
-
ret_list = struct.unpack(f"{opt}H", ret)
|
373
|
-
elif fmt == "uint16":
|
374
|
-
ret = struct.pack(f"{opt}H", value)
|
375
|
-
ret_list = struct.unpack(f"{opt}H", ret)
|
376
|
-
elif fmt == "int32":
|
377
|
-
ret = struct.pack(f"{opt}i", value)
|
378
|
-
ret_list = struct.unpack(f"{opt}HH", ret)
|
379
|
-
elif fmt == "uint32":
|
380
|
-
ret = struct.pack(f"{opt}I", value)
|
381
|
-
ret_list = struct.unpack(f"{opt}HH", ret)
|
382
|
-
elif fmt == "int64":
|
383
|
-
ret = struct.pack(f"{opt}q", value)
|
384
|
-
ret_list = struct.unpack(f"{opt}HHHH", ret)
|
385
|
-
elif fmt == "uint64":
|
386
|
-
ret = struct.pack(f"{opt}Q", value)
|
387
|
-
ret_list = struct.unpack(f"{opt}HHHH", ret)
|
388
|
-
else:
|
389
|
-
ret_list = [0]
|
390
|
-
except Exception:
|
391
|
-
if "16" in fmt:
|
392
|
-
ret_list = [0]
|
393
|
-
elif "32" in fmt:
|
394
|
-
ret_list = [0, 0]
|
395
|
-
else:
|
396
|
-
ret_list = [0, 0, 0, 0]
|
397
|
-
return list(ret_list)
|
398
|
-
|
399
|
-
def ems_do_control(self, data):
|
400
|
-
for do_info in data:
|
401
|
-
do_number = do_info.DoNumber
|
402
|
-
do_value = do_info.DoValue
|
403
|
-
if 1 <= do_number <= 8 and do_value in [0, 1]:
|
404
|
-
path = DeviceInfo.DI_DO_GPIO_MAPPING[f"DO{do_number}"] + "/value"
|
405
|
-
if not os.path.exists(path):
|
406
|
-
app_log.info(f"DO path not exists {do_info}")
|
407
|
-
else:
|
408
|
-
cmd = f"echo {do_value} > {path}"
|
409
|
-
ret = sp.getstatusoutput(cmd)
|
410
|
-
ret = True if ret[0] == 0 else False
|
411
|
-
app_log.info(f"DO {do_info} 控制结果: {ret}")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/core/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/device/types/alarm.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/smart_scene/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/snap_shot/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/__init__.py
RENAMED
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/api/__init__.py
RENAMED
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/api/service.py
RENAMED
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/core/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/test/test_ftp.py
RENAMED
File without changes
|
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/types/client.py
RENAMED
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/upload/types/ftp.py
RENAMED
File without changes
|
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/utils/cloud_client.py
RENAMED
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/utils/common.py
RENAMED
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/utils/struct_util.py
RENAMED
File without changes
|
{solax_py_library-1.0.0.2507 → solax_py_library-1.0.0.2508}/solax_py_library/utils/time_util.py
RENAMED
File without changes
|