@eohjsc/react-native-smart-city 0.7.19 → 0.7.20
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.
- package/package.json +1 -1
- package/src/screens/AddNewGateway/ScanDeviceLocal.js +15 -14
- package/src/screens/AddNewGateway/__test__/ScanDeviceLocal.test.js +51 -1
- package/src/screens/AddNewGateway/hooks/useConnectDevice.js +11 -9
- package/src/screens/AllGateway/DeviceInternalDetail/__test__/index.test.js +28 -0
- package/src/screens/AllGateway/DeviceModbusDetail/__test__/index.test.js +1 -1
- package/src/screens/AllGateway/__test__/index.test.js +19 -0
- package/src/screens/AllGateway/hooks/useGateway.js +16 -9
- package/src/screens/Automate/ScriptDetail/Styles/indexStyles.js +1 -0
- package/src/screens/Automate/ScriptDetail/index.js +43 -34
- package/src/utils/I18n/translations/en.js +3 -0
- package/src/utils/I18n/translations/vi.js +3 -0
package/package.json
CHANGED
|
@@ -30,12 +30,18 @@ import styles from './ScanDeviceLocalStyles';
|
|
|
30
30
|
|
|
31
31
|
let zeroconf;
|
|
32
32
|
|
|
33
|
-
const DeviceItem = ({
|
|
33
|
+
const DeviceItem = ({
|
|
34
|
+
item,
|
|
35
|
+
selectedDevice,
|
|
36
|
+
setSelectedDevice,
|
|
37
|
+
fetchDeviceInfo,
|
|
38
|
+
}) => {
|
|
34
39
|
const handleSelectDevice = useCallback(
|
|
35
40
|
(device) => {
|
|
36
41
|
setSelectedDevice(device);
|
|
42
|
+
fetchDeviceInfo(device?.host);
|
|
37
43
|
},
|
|
38
|
-
[setSelectedDevice]
|
|
44
|
+
[setSelectedDevice, fetchDeviceInfo]
|
|
39
45
|
);
|
|
40
46
|
|
|
41
47
|
return (
|
|
@@ -69,9 +75,8 @@ const ScanDeviceLocal = ({ route }) => {
|
|
|
69
75
|
const [selectedDevice, setSelectedDevice] = useState();
|
|
70
76
|
const [isShowLoading, setIsShowLoading] = useState(false);
|
|
71
77
|
|
|
72
|
-
const { deviceInfo, fetchDeviceInfo, sendConfigToDevice } =
|
|
73
|
-
|
|
74
|
-
);
|
|
78
|
+
const { deviceInfo, fetchDeviceInfo, sendConfigToDevice } =
|
|
79
|
+
useConnectDevice();
|
|
75
80
|
|
|
76
81
|
const code = useMemo(() => {
|
|
77
82
|
return uuidv4();
|
|
@@ -107,7 +112,7 @@ const ScanDeviceLocal = ({ route }) => {
|
|
|
107
112
|
|
|
108
113
|
const onConnectingDevice = useCallback(async () => {
|
|
109
114
|
setIsShowLoading(true);
|
|
110
|
-
const success = await sendConfigToDevice({
|
|
115
|
+
const success = await sendConfigToDevice(selectedDevice?.host, {
|
|
111
116
|
token: code,
|
|
112
117
|
host: detailChipQr?.host,
|
|
113
118
|
port: detailChipQr?.port,
|
|
@@ -126,6 +131,7 @@ const ScanDeviceLocal = ({ route }) => {
|
|
|
126
131
|
code,
|
|
127
132
|
subUnit,
|
|
128
133
|
deviceInfo?.type,
|
|
134
|
+
selectedDevice?.host,
|
|
129
135
|
detailChipQr,
|
|
130
136
|
t,
|
|
131
137
|
sendConfigToDevice,
|
|
@@ -138,12 +144,13 @@ const ScanDeviceLocal = ({ route }) => {
|
|
|
138
144
|
return (
|
|
139
145
|
<DeviceItem
|
|
140
146
|
item={item}
|
|
141
|
-
setSelectedDevice={setSelectedDevice}
|
|
142
147
|
selectedDevice={selectedDevice}
|
|
148
|
+
setSelectedDevice={setSelectedDevice}
|
|
149
|
+
fetchDeviceInfo={fetchDeviceInfo}
|
|
143
150
|
/>
|
|
144
151
|
);
|
|
145
152
|
},
|
|
146
|
-
[selectedDevice, setSelectedDevice]
|
|
153
|
+
[selectedDevice, setSelectedDevice, fetchDeviceInfo]
|
|
147
154
|
);
|
|
148
155
|
|
|
149
156
|
const handleGoBack = useCallback(() => {
|
|
@@ -202,12 +209,6 @@ const ScanDeviceLocal = ({ route }) => {
|
|
|
202
209
|
};
|
|
203
210
|
}, []);
|
|
204
211
|
|
|
205
|
-
useEffect(() => {
|
|
206
|
-
if (selectedDevice) {
|
|
207
|
-
fetchDeviceInfo();
|
|
208
|
-
}
|
|
209
|
-
}, [selectedDevice, fetchDeviceInfo]);
|
|
210
|
-
|
|
211
212
|
useEffect(() => {
|
|
212
213
|
if (deviceInfo) {
|
|
213
214
|
fetchChipQrDetail(deviceInfo?.secret);
|
|
@@ -93,7 +93,7 @@ describe('test ScanDeviceLocal', () => {
|
|
|
93
93
|
await viewButtonBottom.props.onRightClick();
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
expect(deviceApi.
|
|
96
|
+
expect(deviceApi.getBaseURL()).toBe(`http://${resolvedDevice.host}/api`);
|
|
97
97
|
|
|
98
98
|
expect(zeroconfMock.on).toHaveBeenCalledWith(
|
|
99
99
|
'resolved',
|
|
@@ -259,6 +259,56 @@ describe('test ScanDeviceLocal', () => {
|
|
|
259
259
|
|
|
260
260
|
const viewButtonBottom = instance.findByType(ViewButtonBottom);
|
|
261
261
|
expect(viewButtonBottom.props.rightDisabled).toBeTruthy();
|
|
262
|
+
expect(spyToastError).toHaveBeenCalledWith(
|
|
263
|
+
getTranslate('en', 'cannot_fetch_data_from_device')
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('test disable connect button when fetch chip qr info error', async () => {
|
|
268
|
+
const resolvedDevice = {
|
|
269
|
+
host: 'host-1.lan',
|
|
270
|
+
name: 'Device 1',
|
|
271
|
+
type: 'end_device',
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
mock.onGet(API.DEV_MODE.CHIP_QR_CODE.DETAIL_BY_SECRET()).reply(400);
|
|
275
|
+
mockDeviceApi.onGet(DEVICEAPI.BOARD.DETAIL()).reply(200, {
|
|
276
|
+
name: resolvedDevice.name,
|
|
277
|
+
type: resolvedDevice.type,
|
|
278
|
+
secret: '123456',
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const route = { params: { unit: 1 } };
|
|
282
|
+
await act(async () => {
|
|
283
|
+
tree = await renderer.create(wrapComponent(route));
|
|
284
|
+
});
|
|
285
|
+
const instance = tree.root;
|
|
286
|
+
|
|
287
|
+
const resolvedCallback = zeroconfMock.on.mock.calls.find(
|
|
288
|
+
(call) => call[0] === 'resolved'
|
|
289
|
+
)[1];
|
|
290
|
+
await act(async () => {
|
|
291
|
+
await resolvedCallback(resolvedDevice);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const flatList = instance.findByType(FlatList);
|
|
295
|
+
expect(flatList.props.data).toEqual([resolvedDevice]);
|
|
296
|
+
await act(async () => {
|
|
297
|
+
await flatList.props.renderItem({ item: resolvedDevice });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Select first device
|
|
301
|
+
const touchable = flatList.findAllByType(TouchableOpacity);
|
|
302
|
+
expect(touchable).toHaveLength(1);
|
|
303
|
+
await act(async () => {
|
|
304
|
+
await touchable[0].props.onPress();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const viewButtonBottom = instance.findByType(ViewButtonBottom);
|
|
308
|
+
expect(viewButtonBottom.props.rightDisabled).toBeTruthy();
|
|
309
|
+
expect(spyToastError).toHaveBeenCalledWith(
|
|
310
|
+
getTranslate('en', 'chip_qr_not_found')
|
|
311
|
+
);
|
|
262
312
|
});
|
|
263
313
|
|
|
264
314
|
it('test handle connecting device error', async () => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { create } from 'apisauce';
|
|
2
|
-
import { useState, useCallback
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
3
|
|
|
4
4
|
import { API } from '../configs/API';
|
|
5
|
+
import { ToastBottomHelper } from '../../../utils/Utils';
|
|
6
|
+
import t from '../../../hooks/Common/useTranslations';
|
|
5
7
|
|
|
6
8
|
export const api = create({
|
|
7
9
|
timeout: 10000,
|
|
@@ -10,15 +12,11 @@ export const api = create({
|
|
|
10
12
|
},
|
|
11
13
|
});
|
|
12
14
|
|
|
13
|
-
export const useConnectDevice = (
|
|
15
|
+
export const useConnectDevice = () => {
|
|
14
16
|
const [deviceInfo, setDeviceInfo] = useState();
|
|
15
17
|
const [listWiFiDevice, setListWiFiDevice] = useState([]);
|
|
16
18
|
const [lastError, setLastError] = useState();
|
|
17
19
|
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
api.baseURL = `http://${host}/api`;
|
|
20
|
-
}, [host]);
|
|
21
|
-
|
|
22
20
|
const parseResponse = (response, setInfo) => {
|
|
23
21
|
if (response.data?.status === 'error') {
|
|
24
22
|
setLastError(response.data?.message);
|
|
@@ -26,23 +24,27 @@ export const useConnectDevice = (host) => {
|
|
|
26
24
|
}
|
|
27
25
|
if (!response.ok) {
|
|
28
26
|
setLastError(response.problem);
|
|
27
|
+
setInfo && ToastBottomHelper.error(t('cannot_fetch_data_from_device'));
|
|
29
28
|
return false;
|
|
30
29
|
}
|
|
31
30
|
setInfo && setInfo(response.data);
|
|
32
31
|
return true;
|
|
33
32
|
};
|
|
34
33
|
|
|
35
|
-
const fetchDeviceInfo = useCallback(async () => {
|
|
34
|
+
const fetchDeviceInfo = useCallback(async (host) => {
|
|
35
|
+
api.setBaseURL(`http://${host}/api`);
|
|
36
36
|
const response = await api.get(API.BOARD.DETAIL());
|
|
37
37
|
return parseResponse(response, setDeviceInfo);
|
|
38
38
|
}, []);
|
|
39
39
|
|
|
40
|
-
const fetchListWiFiDevice = useCallback(async () => {
|
|
40
|
+
const fetchListWiFiDevice = useCallback(async (host) => {
|
|
41
|
+
api.setBaseURL(`http://${host}/api`);
|
|
41
42
|
const response = await api.get(API.BOARD.SCAN_WIFI());
|
|
42
43
|
return parseResponse(response, setListWiFiDevice);
|
|
43
44
|
}, []);
|
|
44
45
|
|
|
45
|
-
const sendConfigToDevice = useCallback(async (config) => {
|
|
46
|
+
const sendConfigToDevice = useCallback(async (host, config) => {
|
|
47
|
+
api.setBaseURL(`http://${host}/api`);
|
|
46
48
|
const response = await api.post(API.BOARD.CONFIG(), config);
|
|
47
49
|
return parseResponse(response);
|
|
48
50
|
}, []);
|
|
@@ -179,6 +179,34 @@ describe('Test DeviceInternalDetail', () => {
|
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
it('test render DeviceInternalDetail onPress more and onPress Delete internal failure', async () => {
|
|
183
|
+
await act(async () => {
|
|
184
|
+
tree = await create(wrapComponent());
|
|
185
|
+
});
|
|
186
|
+
const instance = tree.root;
|
|
187
|
+
const detail = instance?.findByType(Detail);
|
|
188
|
+
await headerCustomOnPressMore(detail);
|
|
189
|
+
const menuActionMore = detail?.findByType(MenuActionMore);
|
|
190
|
+
expect(menuActionMore.props.isVisible).toEqual(true);
|
|
191
|
+
await act(async () => {
|
|
192
|
+
menuActionMore.props.listMenuItem[1].doAction();
|
|
193
|
+
});
|
|
194
|
+
const modal = instance.findByType(ModalPopupCT);
|
|
195
|
+
|
|
196
|
+
mock.onDelete(API.DEV_MODE.ARDUINO.DEVICE_DETAIL(1, 1)).reply(400);
|
|
197
|
+
await act(async () => {
|
|
198
|
+
await modal.props.onPressConfirm();
|
|
199
|
+
});
|
|
200
|
+
expect(global.mockedPop).not.toHaveBeenCalled();
|
|
201
|
+
expect(Toast.show).toHaveBeenCalledWith({
|
|
202
|
+
position: 'bottom',
|
|
203
|
+
text1: 'CLIENT_ERROR',
|
|
204
|
+
text2: undefined,
|
|
205
|
+
type: 'error',
|
|
206
|
+
visibilityTime: 1000,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
182
210
|
it('test render DeviceInternalDetail onPress TabPanel config write', async () => {
|
|
183
211
|
mock
|
|
184
212
|
.onGet(API.DEV_MODE.ARDUINO.CONFIG_PINS(1, 1))
|
|
@@ -137,7 +137,7 @@ describe('Test DeviceModbusDetail', () => {
|
|
|
137
137
|
await flushPromises();
|
|
138
138
|
|
|
139
139
|
const modal = instance.findByType(ModalPopupCT);
|
|
140
|
-
mock.onDelete(API.DEV_MODE.
|
|
140
|
+
mock.onDelete(API.DEV_MODE.MODBUS.DEVICE_DETAIL(1, 1)).reply(200);
|
|
141
141
|
await act(async () => {
|
|
142
142
|
await modal.props.onPressConfirm();
|
|
143
143
|
});
|
|
@@ -85,6 +85,25 @@ describe('Test Gateway screen', () => {
|
|
|
85
85
|
expect(flatList.props.data).toEqual([{ id: 1, name: 'device 1' }]);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it('render Gateway onLoadMore', async () => {
|
|
89
|
+
await act(async () => {
|
|
90
|
+
tree = await create(wrapComponent());
|
|
91
|
+
});
|
|
92
|
+
const instance = tree.root;
|
|
93
|
+
const flatList = instance.findByType(FlatList);
|
|
94
|
+
expect(flatList.props.data).toEqual([]);
|
|
95
|
+
mock
|
|
96
|
+
.onGet(API.DEV_MODE.GATEWAY.LIST())
|
|
97
|
+
.reply(200, { results: [{ id: 1, name: 'device 1' }] });
|
|
98
|
+
await act(async () => {
|
|
99
|
+
await flatList.props.onMomentumScrollBegin();
|
|
100
|
+
});
|
|
101
|
+
await act(async () => {
|
|
102
|
+
await flatList.props.onEndReached();
|
|
103
|
+
});
|
|
104
|
+
expect(flatList.props.data).toEqual([{ id: 1, name: 'device 1' }]);
|
|
105
|
+
});
|
|
106
|
+
|
|
88
107
|
it('Test render empty', async () => {
|
|
89
108
|
await act(async () => {
|
|
90
109
|
tree = await create(wrapComponent());
|
|
@@ -146,26 +146,29 @@ export const useGateway = () => {
|
|
|
146
146
|
isModbus,
|
|
147
147
|
numberGoBack = 1
|
|
148
148
|
) => {
|
|
149
|
-
let
|
|
149
|
+
let response;
|
|
150
150
|
if (isInternal) {
|
|
151
|
-
|
|
151
|
+
response = await axiosDelete(
|
|
152
152
|
API.DEV_MODE.ARDUINO.DEVICE_DETAIL(gatewayId, deviceId)
|
|
153
153
|
);
|
|
154
|
-
success &&
|
|
154
|
+
response.success &&
|
|
155
|
+
setGatewayDevices((prev) => ({ ...prev, internal: [] }));
|
|
155
156
|
}
|
|
156
157
|
if (isZigbee) {
|
|
157
|
-
|
|
158
|
+
response = await axiosDelete(
|
|
158
159
|
API.DEV_MODE.ZIGBEE.DEVICE_DETAIL(gatewayId, deviceId)
|
|
159
160
|
);
|
|
160
|
-
success &&
|
|
161
|
+
response.success &&
|
|
162
|
+
setGatewayDevices((prev) => ({ ...prev, zigbee: [] }));
|
|
161
163
|
}
|
|
162
164
|
if (isModbus) {
|
|
163
|
-
|
|
165
|
+
response = await axiosDelete(
|
|
164
166
|
API.DEV_MODE.MODBUS.DEVICE_DETAIL(gatewayId, deviceId)
|
|
165
167
|
);
|
|
166
|
-
success &&
|
|
168
|
+
response.success &&
|
|
169
|
+
setGatewayDevices((prev) => ({ ...prev, modbus: [] }));
|
|
167
170
|
}
|
|
168
|
-
if (success) {
|
|
171
|
+
if (response.success) {
|
|
169
172
|
ToastBottomHelper.success(t('delete_successfully'));
|
|
170
173
|
navigation.pop(numberGoBack);
|
|
171
174
|
}
|
|
@@ -259,7 +262,11 @@ export const useGateway = () => {
|
|
|
259
262
|
params: { secret },
|
|
260
263
|
}
|
|
261
264
|
);
|
|
262
|
-
|
|
265
|
+
if (success) {
|
|
266
|
+
setDetailChipQr(data);
|
|
267
|
+
} else {
|
|
268
|
+
ToastBottomHelper.error(t('chip_qr_not_found'));
|
|
269
|
+
}
|
|
263
270
|
}, []);
|
|
264
271
|
|
|
265
272
|
return {
|
|
@@ -5,7 +5,14 @@ import React, {
|
|
|
5
5
|
useRef,
|
|
6
6
|
useState,
|
|
7
7
|
} from 'react';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Image,
|
|
10
|
+
Platform,
|
|
11
|
+
ScrollView,
|
|
12
|
+
Switch,
|
|
13
|
+
TouchableOpacity,
|
|
14
|
+
View,
|
|
15
|
+
} from 'react-native';
|
|
9
16
|
import { PopoverMode } from 'react-native-popover-view';
|
|
10
17
|
import { IconFill, IconOutline } from '@ant-design/icons-react-native';
|
|
11
18
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
|
@@ -550,46 +557,48 @@ const ScriptDetail = ({ route }) => {
|
|
|
550
557
|
onBackdropPress={onCloseLocalControl}
|
|
551
558
|
>
|
|
552
559
|
<View key={'localControl'} style={styles.popoverStyle}>
|
|
553
|
-
<
|
|
554
|
-
style={styles.textDisable}
|
|
555
|
-
key={'listChip'}
|
|
556
|
-
onPress={() => {
|
|
557
|
-
onPressSelectChip(false, {
|
|
558
|
-
id: local_control.chip_id_local_control,
|
|
559
|
-
name: local_control.chip_local_control,
|
|
560
|
-
});
|
|
561
|
-
}}
|
|
562
|
-
accessibilityLabel={
|
|
563
|
-
AccessibilityLabel.AUTOMATE_DISABLE_LOCAL_CONTROL
|
|
564
|
-
}
|
|
565
|
-
>
|
|
566
|
-
<Text>{t('disable')}</Text>
|
|
567
|
-
{!local_control.is_local_control && (
|
|
568
|
-
<IconOutline style={styles.checked} name={'check'} size={20} />
|
|
569
|
-
)}
|
|
570
|
-
</TouchableOpacity>
|
|
571
|
-
{listChipShared.map((item, index) => (
|
|
560
|
+
<ScrollView>
|
|
572
561
|
<TouchableOpacity
|
|
573
|
-
|
|
574
|
-
|
|
562
|
+
style={styles.textDisable}
|
|
563
|
+
key={'listChip'}
|
|
575
564
|
onPress={() => {
|
|
576
|
-
onPressSelectChip(
|
|
565
|
+
onPressSelectChip(false, {
|
|
566
|
+
id: local_control.chip_id_local_control,
|
|
567
|
+
name: local_control.chip_local_control,
|
|
568
|
+
});
|
|
577
569
|
}}
|
|
578
570
|
accessibilityLabel={
|
|
579
|
-
AccessibilityLabel.
|
|
571
|
+
AccessibilityLabel.AUTOMATE_DISABLE_LOCAL_CONTROL
|
|
580
572
|
}
|
|
581
573
|
>
|
|
582
|
-
<Text>{
|
|
583
|
-
{local_control.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
style={styles.checked}
|
|
587
|
-
name={'check'}
|
|
588
|
-
size={20}
|
|
589
|
-
/>
|
|
590
|
-
)}
|
|
574
|
+
<Text>{t('disable')}</Text>
|
|
575
|
+
{!local_control.is_local_control && (
|
|
576
|
+
<IconOutline style={styles.checked} name={'check'} size={20} />
|
|
577
|
+
)}
|
|
591
578
|
</TouchableOpacity>
|
|
592
|
-
|
|
579
|
+
{listChipShared.map((item, index) => (
|
|
580
|
+
<TouchableOpacity
|
|
581
|
+
key={'listChip' + index} // Add key fix warning
|
|
582
|
+
style={styles.listChip}
|
|
583
|
+
onPress={() => {
|
|
584
|
+
onPressSelectChip(true, item);
|
|
585
|
+
}}
|
|
586
|
+
accessibilityLabel={
|
|
587
|
+
AccessibilityLabel.AUTOMATE_ENABLE_LOCAL_CONTROL
|
|
588
|
+
}
|
|
589
|
+
>
|
|
590
|
+
<Text>{item.name}</Text>
|
|
591
|
+
{local_control.chip_id_local_control === item.id &&
|
|
592
|
+
local_control.is_local_control && (
|
|
593
|
+
<IconOutline
|
|
594
|
+
style={styles.checked}
|
|
595
|
+
name={'check'}
|
|
596
|
+
size={20}
|
|
597
|
+
/>
|
|
598
|
+
)}
|
|
599
|
+
</TouchableOpacity>
|
|
600
|
+
))}
|
|
601
|
+
</ScrollView>
|
|
593
602
|
</View>
|
|
594
603
|
</ModalCustom>
|
|
595
604
|
</View>
|
|
@@ -1536,4 +1536,7 @@ export default {
|
|
|
1536
1536
|
'Press and hold the number and drag to rearrange the order of the Sub-Units',
|
|
1537
1537
|
rearrange_sub_unit: 'Rearrange Sub-Unit',
|
|
1538
1538
|
updated_sub_unit_order: 'Updated Sub-Units order successfully!',
|
|
1539
|
+
cannot_fetch_data_from_device:
|
|
1540
|
+
'Cannot fetch data from device, Please try again',
|
|
1541
|
+
chip_qr_not_found: 'Chip not found in the system',
|
|
1539
1542
|
};
|
|
@@ -1542,4 +1542,7 @@ export default {
|
|
|
1542
1542
|
'Nhấn giữ vào số thứ tự và di chuyển để sắp xếp lại thứ tự các khu vực',
|
|
1543
1543
|
rearrange_sub_unit: 'Sắp xếp lại các khu vực',
|
|
1544
1544
|
updated_sub_unit_order: 'Đã cập nhật thứ tự các khu vực thành công!',
|
|
1545
|
+
cannot_fetch_data_from_device:
|
|
1546
|
+
'Không thể lấy dữ liệu từ thiết bị, Vui lòng thử lại',
|
|
1547
|
+
chip_qr_not_found: 'Không tìm thấy chip trong hệ thống',
|
|
1545
1548
|
};
|