solax-py-library 1.0.0.2502__py3-none-any.whl → 1.0.0.2504__py3-none-any.whl
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/device/constant/cabinet.py +2 -0
- solax_py_library/device/types/alarm.py +16 -0
- solax_py_library/smart_scene/constant/message_entry.py +174 -336
- solax_py_library/smart_scene/core/action/base.py +9 -0
- solax_py_library/smart_scene/core/action/ems_action.py +19 -0
- solax_py_library/smart_scene/core/action/system_action.py +241 -0
- solax_py_library/smart_scene/core/condition/base.py +8 -7
- solax_py_library/smart_scene/core/condition/cabinet_condition.py +11 -77
- solax_py_library/smart_scene/core/condition/date_condition.py +9 -9
- solax_py_library/smart_scene/core/condition/price_condition.py +27 -33
- solax_py_library/smart_scene/core/condition/system_condition.py +14 -21
- solax_py_library/smart_scene/core/condition/weather_condition.py +13 -22
- solax_py_library/smart_scene/core/service/__init__.py +0 -0
- solax_py_library/smart_scene/core/service/smart_scene_service.py +411 -0
- solax_py_library/smart_scene/types/action.py +3 -3
- solax_py_library/smart_scene/types/condition.py +32 -26
- solax_py_library/smart_scene/types/smart_scene_content.py +9 -9
- solax_py_library/test/test_utils/test_cloud_client.py +1 -1
- solax_py_library/utils/cloud_client.py +2 -2
- solax_py_library/utils/time_util.py +33 -0
- {solax_py_library-1.0.0.2502.dist-info → solax_py_library-1.0.0.2504.dist-info}/METADATA +1 -1
- {solax_py_library-1.0.0.2502.dist-info → solax_py_library-1.0.0.2504.dist-info}/RECORD +23 -16
- {solax_py_library-1.0.0.2502.dist-info → solax_py_library-1.0.0.2504.dist-info}/WHEEL +0 -0
@@ -0,0 +1,411 @@
|
|
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}")
|
@@ -3,8 +3,8 @@ from typing import List, Union
|
|
3
3
|
|
4
4
|
from pydantic import BaseModel
|
5
5
|
|
6
|
-
from
|
7
|
-
from
|
6
|
+
from solax_py_library.device.constant.cabinet import TRENE_CABINET_ENUM
|
7
|
+
from solax_py_library.smart_scene.constant.message_entry import MESSAGE_ENTRY
|
8
8
|
|
9
9
|
|
10
10
|
class ActionType(str, Enum):
|
@@ -59,7 +59,7 @@ class SystemActionItemData(BaseModel):
|
|
59
59
|
if self.childData.data[0] == 0:
|
60
60
|
return MESSAGE_ENTRY["importControlOff"][lang]
|
61
61
|
else:
|
62
|
-
if cabinet_type in
|
62
|
+
if cabinet_type in TRENE_CABINET_ENUM:
|
63
63
|
msg = (
|
64
64
|
"importControl_standby"
|
65
65
|
if self.childData.data[1] == 0
|
@@ -1,8 +1,10 @@
|
|
1
|
+
import operator
|
1
2
|
from enum import IntEnum, Enum
|
2
3
|
from typing import Optional, List, Union, Any
|
3
4
|
|
4
5
|
from pydantic import BaseModel, validator, root_validator
|
5
6
|
|
7
|
+
from solax_py_library.device.types.alarm import AlarmLevel
|
6
8
|
from solax_py_library.smart_scene.constant.message_entry import MESSAGE_ENTRY
|
7
9
|
|
8
10
|
|
@@ -16,6 +18,13 @@ class ConditionFunc(IntEnum):
|
|
16
18
|
LT = 101
|
17
19
|
EQ = 102
|
18
20
|
|
21
|
+
def function(self):
|
22
|
+
return {
|
23
|
+
ConditionFunc.GT: operator.gt,
|
24
|
+
ConditionFunc.LT: operator.lt,
|
25
|
+
ConditionFunc.EQ: operator.eq,
|
26
|
+
}.get(self)
|
27
|
+
|
19
28
|
|
20
29
|
class RepeatFunc(IntEnum):
|
21
30
|
ONCE = 103
|
@@ -79,13 +88,13 @@ class PriceConditionItemData(BaseModel):
|
|
79
88
|
|
80
89
|
@validator("childData", always=True)
|
81
90
|
def _check_child_data(cls, value, values):
|
82
|
-
|
83
|
-
if
|
91
|
+
child_type = values.get("childType")
|
92
|
+
if child_type in {
|
84
93
|
PriceConditionType.lowerPrice,
|
85
94
|
PriceConditionType.higherPrice,
|
86
95
|
}:
|
87
96
|
assert value.data[0] > 0, ValueError
|
88
|
-
elif
|
97
|
+
elif child_type in {
|
89
98
|
PriceConditionType.expensiveHours,
|
90
99
|
PriceConditionType.cheapestHours,
|
91
100
|
}:
|
@@ -119,14 +128,14 @@ class SystemConditionItemData(BaseModel):
|
|
119
128
|
|
120
129
|
@validator("childData", always=True)
|
121
130
|
def _check_child_data(cls, value, values):
|
122
|
-
|
123
|
-
if
|
131
|
+
child_type = values.get("childType")
|
132
|
+
if child_type in {
|
124
133
|
SystemConditionType.systemExportPower,
|
125
134
|
SystemConditionType.systemImportPower,
|
126
135
|
}:
|
127
136
|
assert 0 <= value.data[0] <= 100000, ValueError
|
128
137
|
value.data[0] = round(value.data[0], 2) # 功率保留两位小数
|
129
|
-
elif
|
138
|
+
elif child_type == SystemConditionType.systemSoc:
|
130
139
|
assert 5 <= value.data[0] <= 100, ValueError
|
131
140
|
return value
|
132
141
|
|
@@ -152,14 +161,14 @@ class CabinetConditionItemData(BaseModel):
|
|
152
161
|
|
153
162
|
@validator("childData", always=True)
|
154
163
|
def _check_child_data(cls, value, values):
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
if
|
164
|
+
child_type = values.get("childType")
|
165
|
+
if child_type == CabinetConditionType.cabinetAlarm:
|
166
|
+
assert value.data[0] in {
|
167
|
+
AlarmLevel.TIPS,
|
168
|
+
AlarmLevel.NORMAL,
|
169
|
+
AlarmLevel.EMERGENCY,
|
170
|
+
}, ValueError
|
171
|
+
if child_type == CabinetConditionType.cabinetSoc:
|
163
172
|
assert 0 <= value.data[0] <= 100, ValueError
|
164
173
|
return value
|
165
174
|
|
@@ -182,11 +191,11 @@ class DateConditionItemData(BaseModel):
|
|
182
191
|
|
183
192
|
@validator("childData", always=True)
|
184
193
|
def check_param(cls, value, values):
|
185
|
-
|
194
|
+
child_type = values.get("childType")
|
186
195
|
data = value.data
|
187
|
-
if
|
196
|
+
if child_type == DateConditionType.time:
|
188
197
|
assert isinstance(data[0], str), ValueError
|
189
|
-
elif
|
198
|
+
elif child_type == DateConditionType.duration:
|
190
199
|
assert isinstance(data[0], int), ValueError
|
191
200
|
return value
|
192
201
|
|
@@ -203,8 +212,8 @@ class WeatherConditionItemData(BaseModel):
|
|
203
212
|
|
204
213
|
@validator("childData", always=True)
|
205
214
|
def _check_child_data(cls, value, values):
|
206
|
-
|
207
|
-
if
|
215
|
+
child_type = values.get("childType")
|
216
|
+
if child_type == WeatherConditionType.irradiance:
|
208
217
|
assert value.data[0] > 0, ValueError
|
209
218
|
assert 0 <= value.data[1] <= 24, ValueError
|
210
219
|
return value
|
@@ -247,7 +256,9 @@ class ConditionItem(BaseModel):
|
|
247
256
|
return {self.type: [d.to_text(lang, unit) for d in self.data]}
|
248
257
|
elif self.type == ConditionType.cabinet:
|
249
258
|
cabinet_sns = ",".join(self.cabinet)
|
250
|
-
return {
|
259
|
+
return {
|
260
|
+
self.type: [d.to_text(lang, unit) for d in self.data] + [cabinet_sns]
|
261
|
+
}
|
251
262
|
|
252
263
|
|
253
264
|
class SmartSceneCondition(BaseModel):
|
@@ -273,13 +284,8 @@ class SmartSceneCondition(BaseModel):
|
|
273
284
|
return values
|
274
285
|
|
275
286
|
def to_text(self, lang, unit):
|
276
|
-
|
277
|
-
# "operation": MESSAGE_ENTRY[self.operation.name][lang],
|
278
|
-
# "value": {}
|
279
|
-
# }
|
280
|
-
ret = {}
|
287
|
+
ret = {"operation": [MESSAGE_ENTRY[self.operation.name][lang]]}
|
281
288
|
for v in self.value:
|
282
|
-
# ret["value"].update(v.to_text(lang, unit))
|
283
289
|
ret.update(v.to_text(lang, unit))
|
284
290
|
return ret
|
285
291
|
|
@@ -5,21 +5,21 @@ from typing import Optional, List
|
|
5
5
|
from pydantic import Field
|
6
6
|
from pydantic.main import BaseModel
|
7
7
|
|
8
|
-
from
|
8
|
+
from solax_py_library.device.constant.cabinet import TRENE_CABINET_ENUM
|
9
|
+
from solax_py_library.smart_scene.constant.message_entry import MESSAGE_ENTRY
|
10
|
+
from solax_py_library.smart_scene.types.action import (
|
11
|
+
SmartSceneAction,
|
9
12
|
ActionType,
|
10
13
|
SystemActionType,
|
11
|
-
SmartSceneAction,
|
12
14
|
)
|
13
|
-
from
|
14
|
-
from domain.ems_enum.smart_scene.condition import (
|
15
|
-
LogicFunc,
|
16
|
-
ConditionFunc,
|
15
|
+
from solax_py_library.smart_scene.types.condition import (
|
17
16
|
RepeatFunc,
|
17
|
+
SmartSceneCondition,
|
18
|
+
LogicFunc,
|
18
19
|
ConditionType,
|
19
20
|
PriceConditionType,
|
20
|
-
|
21
|
+
ConditionFunc,
|
21
22
|
)
|
22
|
-
from settings.const import DeviceInfo
|
23
23
|
|
24
24
|
|
25
25
|
class SmartSceneOtherInfo(BaseModel):
|
@@ -145,7 +145,7 @@ class SmartSceneContent(BaseModel):
|
|
145
145
|
],
|
146
146
|
"elseThen": [],
|
147
147
|
}
|
148
|
-
if cabinet_type in
|
148
|
+
if cabinet_type in TRENE_CABINET_ENUM:
|
149
149
|
return [scene_rec_2]
|
150
150
|
else:
|
151
151
|
return [scene_rec_1, scene_rec_2]
|
@@ -20,7 +20,7 @@ class CloudClient:
|
|
20
20
|
"registrationSn": ems_sn,
|
21
21
|
"snSecret": sn_secret,
|
22
22
|
},
|
23
|
-
timeout=5
|
23
|
+
timeout=5,
|
24
24
|
)
|
25
25
|
if response.content:
|
26
26
|
response_data = json.loads(response.content)
|
@@ -144,7 +144,7 @@ class CloudClient:
|
|
144
144
|
url=price_url,
|
145
145
|
headers={"token": token, "Content-Type": "application/json"},
|
146
146
|
json={"registerNo": ems_sn},
|
147
|
-
timeout=5
|
147
|
+
timeout=5,
|
148
148
|
)
|
149
149
|
# 访问失败或获取数据失败,则重复插入最后一条数据
|
150
150
|
if response.status_code != 200:
|
@@ -1,5 +1,38 @@
|
|
1
|
+
from datetime import datetime, timedelta
|
2
|
+
|
3
|
+
|
1
4
|
def trans_str_time_to_index(now_time, minute=15):
|
2
5
|
"""将时间按照minute切换为索引,时间格式为 %H-%M"""
|
3
6
|
time_list = [int(i) for i in now_time.split(":")]
|
4
7
|
time_int = time_list[0] * 4 + time_list[1] // minute
|
5
8
|
return time_int
|
9
|
+
|
10
|
+
|
11
|
+
def get_highest_or_lowest_value(start_time, end_time, hours, price_list, reverse=False):
|
12
|
+
start_index = trans_str_time_to_index(start_time)
|
13
|
+
end_index = trans_str_time_to_index(end_time)
|
14
|
+
arr = price_list[start_index:end_index]
|
15
|
+
if None in arr:
|
16
|
+
return False
|
17
|
+
indices = list(range(end_index - start_index))
|
18
|
+
sorted_indices = sorted(indices, key=lambda i: arr[i], reverse=reverse)
|
19
|
+
return sorted_indices[: int(hours * 4)], start_index
|
20
|
+
|
21
|
+
|
22
|
+
def get_rounded_times():
|
23
|
+
"""
|
24
|
+
返回距离当前时间最近的15min的整点时间以及后一整点5min时间(天气是预测未来15min的,也就是在00:00时,只能拿到00:15的数据)
|
25
|
+
"""
|
26
|
+
now = datetime.now()
|
27
|
+
# 确定当前时间所属的15分钟区间
|
28
|
+
index_1 = now.minute // 15
|
29
|
+
index_2 = now.minute % 15
|
30
|
+
left_time = now.replace(minute=15 * index_1, second=0, microsecond=0)
|
31
|
+
right_time = left_time + timedelta(minutes=15)
|
32
|
+
if index_2 < 8:
|
33
|
+
nearest_time = left_time
|
34
|
+
else:
|
35
|
+
nearest_time = right_time
|
36
|
+
return datetime.strftime(nearest_time, "%Y-%m-%d %H:%M:%S"), datetime.strftime(
|
37
|
+
right_time, "%Y-%m-%d %H:%M:%S"
|
38
|
+
)
|