@eohjsc/react-native-smart-city 0.3.10 → 0.3.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eohjsc/react-native-smart-city",
3
3
  "title": "React Native Smart Home",
4
- "version": "0.3.10",
4
+ "version": "0.3.11",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { View } from 'react-native';
3
+ import { useNavigation } from '@react-navigation/native';
3
4
  import { useTranslations } from '../../../hooks/Common/useTranslations';
4
5
  import { useSensorsStatus } from '../../../hooks/Common';
5
6
 
@@ -8,7 +9,7 @@ import ItemDevice from '../../Device/ItemDevice';
8
9
  import ItemOneTap from '../OneTap/ItemOneTap';
9
10
  import ItemAddNew from '../../Device/ItemAddNew';
10
11
  import styles from './styles';
11
- import { notImplemented } from '../../../utils/Utils';
12
+ import Routes from '../../../utils/Route';
12
13
 
13
14
  const SubUnitFavorites = ({
14
15
  isOwner,
@@ -18,11 +19,14 @@ const SubUnitFavorites = ({
18
19
  wrapItemStyle,
19
20
  }) => {
20
21
  const t = useTranslations();
22
+ const { navigate } = useNavigation();
21
23
 
22
24
  const { getStatus, serverDown } = useSensorsStatus(unit, favoriteDevices);
23
25
 
24
26
  const handleOnAddNew = () => {
25
- notImplemented(t);
27
+ navigate(Routes.SelectDevices, {
28
+ unitId: unit.id,
29
+ });
26
30
  };
27
31
 
28
32
  return (
@@ -29,6 +29,9 @@ const API = {
29
29
  CHANGE_OWNER: (id) => `/property_manager/units/${id}/change_owner/`,
30
30
  FAVOURITE_DEVICES: (id) =>
31
31
  `/property_manager/units/${id}/favourite_devices/`,
32
+ DEVICES: (id) => `/property_manager/units/${id}/devices/`,
33
+ ADD_DEVICES_TO_FAVORITES: (id) =>
34
+ `/property_manager/units/${id}/add_devices_to_favourites/`,
32
35
  },
33
36
  SUB_UNIT: {
34
37
  REMOVE_SUB_UNIT: (unitId, id) =>
@@ -173,6 +176,7 @@ const API = {
173
176
  `/notifications/eoh/?page=${page}&type=${type}`,
174
177
  SET_READ: (id) => `/notifications/eoh/${id}/set_read/`,
175
178
  },
179
+ VALUE_EVALUATIONS: () => '/property_manager/config_value_evaluations/',
176
180
  EXTERNAL: {
177
181
  GOOGLE_MAP: {
178
182
  AUTO_COMPLETE:
@@ -12,14 +12,15 @@ export const Action = {
12
12
  CAMERA_STATUS_CHANGE: 'CAMERA_STATUS_CHANGE',
13
13
  CLOSE_ALL_CAMERA: 'CLOSE_ALL_CAMERA',
14
14
  SET_FAVORITE_DEVICES: 'SET_FAVORITE_DEVICES',
15
- ADD_DEVICE_TO_FAVORITES: 'ADD_DEVICE_TO_FAVORITES',
16
- REMOVE_DEVICE_FROM_FAVORITES: 'REMOVE_DEVICE_FROM_FAVORITES',
15
+ ADD_DEVICES_TO_FAVORITES: 'ADD_DEVICE_TO_FAVORITES',
16
+ REMOVE_DEVICES_FROM_FAVORITES: 'REMOVE_DEVICE_FROM_FAVORITES',
17
17
  SET_STARRED_SCRIPTS: 'SET_STARRED_SCRIPTS',
18
18
  STAR_SCRIPT: 'STAR_SCRIPT',
19
19
  UNSTAR_SCRIPT: 'UNSTAR_SCRIPT',
20
20
  CONNECTING_GOOGLE_HOME: 'CONNECTING_GOOGLE_HOME',
21
21
  SET_GOOGLE_HOME_CONNECTIONS: 'SET_GOOGLE_HOME_CONNECTIONS',
22
22
  CHANGE_GOOGLE_HOME_CONN_STATE: 'CHANGE_GOOGLE_HOME_CONN_STATE',
23
+ UPDATE_VALUE_EVALUATIONS: 'UPDATE_VALUE_EVALUATIONS',
23
24
  };
24
25
 
25
26
  export type AuthData = {
@@ -31,11 +31,23 @@ export const mockDataStore: ContextData = {
31
31
  automate: {
32
32
  starredScriptIds: [],
33
33
  },
34
+ app: {
35
+ isFirstOpenCamera: true,
36
+ isLavidaSource: false,
37
+ isConnectWifiGateway: false,
38
+ isBluetoothEnabled: true,
39
+ isNetworkConnected: true,
40
+ camera_opened: [],
41
+ },
34
42
  iot: {
35
- isFirstTimeConnect: true,
36
- isConnecting: false,
37
- googlehome: {},
43
+ googlehome: {
44
+ isFirstTimeConnect: true,
45
+ isConnecting: false,
46
+ connections: {},
47
+ },
38
48
  },
49
+ valueEvaluations: {},
50
+ fetchedValueEvaluationUnits: [],
39
51
  };
40
52
 
41
53
  export const mockSCStore = (data: ContextData): ContextData => {
@@ -83,5 +95,7 @@ export const mockSCStore = (data: ContextData): ContextData => {
83
95
  connections: {},
84
96
  },
85
97
  },
98
+ valueEvaluations: {},
99
+ fetchedValueEvaluationUnits: [],
86
100
  };
87
101
  };
@@ -12,7 +12,7 @@ import {
12
12
  AppType,
13
13
  IoTType,
14
14
  } from './actionType';
15
- import { uniq } from 'lodash';
15
+ import { uniq, reduce } from 'lodash';
16
16
 
17
17
  export type ContextData = {
18
18
  auth: AuthData;
@@ -24,6 +24,8 @@ export type ContextData = {
24
24
  automate: AutomateType;
25
25
  app: AppType;
26
26
  iot: IoTType;
27
+ valueEvaluations: {};
28
+ fetchedValueEvaluationUnits: Array<number>;
27
29
  };
28
30
 
29
31
  export type Action = {
@@ -65,6 +67,8 @@ export const initialState = {
65
67
  connections: {},
66
68
  },
67
69
  },
70
+ valueEvaluations: {},
71
+ fetchedValueEvaluationUnits: [],
68
72
  };
69
73
 
70
74
  export const reducer = (currentState: ContextData, action: Action) => {
@@ -201,7 +205,7 @@ export const reducer = (currentState: ContextData, action: Action) => {
201
205
  favoriteDeviceIds: payload,
202
206
  },
203
207
  };
204
- case Action.ADD_DEVICE_TO_FAVORITES:
208
+ case Action.ADD_DEVICES_TO_FAVORITES:
205
209
  return {
206
210
  ...currentState,
207
211
  unit: {
@@ -211,13 +215,13 @@ export const reducer = (currentState: ContextData, action: Action) => {
211
215
  ),
212
216
  },
213
217
  };
214
- case Action.REMOVE_DEVICE_FROM_FAVORITES:
218
+ case Action.REMOVE_DEVICES_FROM_FAVORITES:
215
219
  return {
216
220
  ...currentState,
217
221
  unit: {
218
222
  ...currentState.unit,
219
223
  favoriteDeviceIds: currentState.unit.favoriteDeviceIds.filter(
220
- (deviceId) => deviceId !== payload
224
+ (deviceId) => !payload.includes(deviceId)
221
225
  ),
222
226
  },
223
227
  };
@@ -294,6 +298,26 @@ export const reducer = (currentState: ContextData, action: Action) => {
294
298
  },
295
299
  };
296
300
 
301
+ case Action.UPDATE_VALUE_EVALUATIONS:
302
+ // eslint-disable-next-line no-case-declarations
303
+ const { data, unitId } = payload;
304
+ return {
305
+ ...currentState,
306
+ fetchedValueEvaluationUnits:
307
+ currentState.fetchedValueEvaluationUnits.indexOf(unitId) !== -1
308
+ ? currentState.fetchedValueEvaluationUnits
309
+ : [...currentState.fetchedValueEvaluationUnits, unitId],
310
+ valueEvaluations: reduce(
311
+ data,
312
+ (dict, item) => {
313
+ // eslint-disable-next-line no-param-reassign
314
+ dict[item.config] = item;
315
+ return dict;
316
+ },
317
+ currentState.valueEvaluations
318
+ ),
319
+ };
320
+
297
321
  default:
298
322
  return currentState;
299
323
  }
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import { renderHook } from '@testing-library/react-hooks';
3
+ import MockAdapter from 'axios-mock-adapter';
4
+ import { SCProvider } from '../../../context';
5
+ import { mockSCStore } from '../../../context/mockStore';
6
+ import { useValueEvaluations } from '../index';
7
+ import api from '../../../utils/Apis/axios';
8
+ import { API } from '../../../configs';
9
+
10
+ const mock = new MockAdapter(api.axiosInstance);
11
+
12
+ const mockedSetAction = jest.fn();
13
+
14
+ const wrapper = ({ children }) => <SCProvider>{children}</SCProvider>;
15
+
16
+ const mockUseContext = jest.fn().mockImplementation(() => ({
17
+ stateData: mockSCStore({}),
18
+ setAction: mockedSetAction,
19
+ }));
20
+
21
+ React.useContext = mockUseContext;
22
+
23
+ describe('Test useValueEvaluation', () => {
24
+ beforeEach(() => {
25
+ mock.resetHistory();
26
+ });
27
+
28
+ it('test unitId null', async () => {
29
+ renderHook(() => useValueEvaluations(null), {
30
+ wrapper,
31
+ });
32
+ expect(mock.history.get.length).toBe(0);
33
+ });
34
+
35
+ it('test unitId not null request success', async () => {
36
+ mock.onGet(API.VALUE_EVALUATIONS()).replyOnce(200, {
37
+ results: [],
38
+ next: 'link',
39
+ });
40
+ mock.onGet(API.VALUE_EVALUATIONS()).replyOnce(200, {
41
+ results: [],
42
+ });
43
+ renderHook(() => useValueEvaluations(1), {
44
+ wrapper,
45
+ });
46
+ expect(mock.history.get.length).toBe(1);
47
+ });
48
+
49
+ it('test request failed', async () => {
50
+ mock.onGet(API.VALUE_EVALUATIONS()).replyOnce(400, {
51
+ results: [],
52
+ });
53
+ renderHook(() => useValueEvaluations(1), {
54
+ wrapper,
55
+ });
56
+ expect(mock.history.get.length).toBe(1);
57
+ });
58
+ });
@@ -1,4 +1,5 @@
1
1
  import useGGHomeConnection from './useGGHomeConnection';
2
2
  import useRemoteControl from './useRemoteControl';
3
+ import useValueEvaluations from './useValueEvaluation';
3
4
 
4
- export { useGGHomeConnection, useRemoteControl };
5
+ export { useGGHomeConnection, useRemoteControl, useValueEvaluations };
@@ -0,0 +1,45 @@
1
+ import { useCallback, useContext, useEffect } from 'react';
2
+ import { API } from '../../configs';
3
+ import { SCContext, useSCContextSelector } from '../../context';
4
+ import { Action } from '../../context/actionType';
5
+ import { axiosGet } from '../../utils/Apis/axios';
6
+
7
+ const useValueEvaluations = (unitId) => {
8
+ const { setAction } = useContext(SCContext);
9
+
10
+ const fetchConfigValueEvaluations = useCallback(
11
+ async (page = 1) => {
12
+ if (!unitId) {
13
+ return;
14
+ }
15
+ const params = new URLSearchParams();
16
+ params.append('config__end_device__station__unit', unitId);
17
+ params.append('page', page);
18
+ const { success, data } = await axiosGet(API.VALUE_EVALUATIONS(), {
19
+ params,
20
+ });
21
+ if (success) {
22
+ setAction(Action.UPDATE_VALUE_EVALUATIONS, {
23
+ unitId,
24
+ data: data.results,
25
+ });
26
+ if (data.next) {
27
+ await fetchConfigValueEvaluations(page + 1);
28
+ }
29
+ }
30
+ },
31
+ [unitId, setAction]
32
+ );
33
+
34
+ const fetchedValueEvaluationUnits = useSCContextSelector((state) => {
35
+ return state.fetchedValueEvaluationUnits || [];
36
+ });
37
+
38
+ useEffect(() => {
39
+ if (!(fetchedValueEvaluationUnits.indexOf(unitId) !== -1)) {
40
+ fetchConfigValueEvaluations();
41
+ }
42
+ }, [unitId, fetchConfigValueEvaluations, fetchedValueEvaluationUnits]);
43
+ };
44
+
45
+ export default useValueEvaluations;
@@ -51,6 +51,7 @@ import EmergencySetting from '../screens/EmergencySetting';
51
51
  import ConfirmUnitDeletion from '../screens/ConfirmUnitDeletion';
52
52
  import InfoMemberUnit from '../screens/Sharing/InfoMemberUnit';
53
53
  import EnterPassword from '../screens/EnterPassword';
54
+ import SelectDevices from '../screens/Unit/SelectDevices';
54
55
  import { HanetCameraStack } from './HanetCameraStack';
55
56
  import { axiosGet } from '../utils/Apis/axios';
56
57
  import { API } from '../configs';
@@ -400,6 +401,13 @@ export const UnitStack = memo((props) => {
400
401
  headerShown: false,
401
402
  }}
402
403
  />
404
+ <Stack.Screen
405
+ name={Route.SelectDevices}
406
+ component={SelectDevices}
407
+ options={{
408
+ headerShown: false,
409
+ }}
410
+ />
403
411
  </Stack.Navigator>
404
412
  );
405
413
  });
@@ -134,6 +134,7 @@ const SelectAction = memo(({ route }) => {
134
134
  value: itemTemp?.value,
135
135
  config_id: itemTemp?.id,
136
136
  config_name: itemTemp?.name,
137
+ sensor_type: itemTemp?.sensor_type,
137
138
  },
138
139
  scriptName,
139
140
  });
@@ -300,9 +301,9 @@ const SelectAction = memo(({ route }) => {
300
301
  if (isNumberValue) {
301
302
  return `${item?.name} ${
302
303
  item?.title ? item.title : t('is_below') + ' (<)'
303
- }{' '}${item?.value} ${item?.unit}`;
304
+ } ${item?.value} ${item?.unit}`;
304
305
  }
305
- return t(stateConditionData?.stateValue[item?.value === 1 ? 1 : 0]);
306
+ return t(stateConditionData?.stateValue[item?.value]);
306
307
  },
307
308
  [t]
308
309
  );
@@ -56,7 +56,7 @@ const SetUpSensor = () => {
56
56
  modalNumberConditionData[0]
57
57
  );
58
58
  const [itemActiveStateModal, setItemActiveStateModal] = useState(
59
- modalStateConditionData?.stateValue[0]
59
+ modalStateConditionData?.stateValue[1]
60
60
  );
61
61
  const [value, setValue] = useState(parseFloat(item?.value || 0));
62
62
  const [minimum] = useState(isHasLimit ? parseInt(item?.range_min, 10) : 0);
@@ -226,7 +226,8 @@ const SetUpSensor = () => {
226
226
  <View style={styles.modalContent}>
227
227
  {(isNumberValue
228
228
  ? modalNumberConditionData
229
- : modalStateConditionData?.stateValue
229
+ : /* Disable picking not_active/not_dectect option -> temporaly remove not_acitve option */
230
+ modalStateConditionData?.stateValue.slice(1)
230
231
  ).map((i, index) => (
231
232
  <>
232
233
  <TouchableOpacity
@@ -44,12 +44,13 @@ import {
44
44
  useBoolean,
45
45
  useGGHomeDeviceConnected,
46
46
  } from '../../hooks/Common';
47
- import { useGGHomeConnection } from '../../hooks/IoT';
47
+ import { useGGHomeConnection, useValueEvaluations } from '../../hooks/IoT';
48
48
  import { SensorDisplayItem } from './components/SensorDisplayItem';
49
49
  import { useSCContextSelector } from '../../context';
50
50
  import { EmergencyCountdown } from './components/EmergencyCountdown';
51
51
  import { SensorConnectStatusViewHeader } from './components/SensorConnectStatusViewHeader';
52
52
  import { useDisconnectedDevice } from './hooks/useDisconnectedDevice';
53
+ import { useEvaluateValue } from './hooks/useEvaluateValue';
53
54
  import { Card } from '../../commons/CardShadow';
54
55
  import PreventAccess from '../../commons/PreventAccess';
55
56
  import { notImplemented } from '../../utils/Utils';
@@ -117,6 +118,8 @@ const DeviceDetail = ({ route }) => {
117
118
  });
118
119
  }, [display]);
119
120
 
121
+ useValueEvaluations(unitId || unitData?.id);
122
+
120
123
  useDisconnectedDevice(sensorName, isDeviceHasBle, serverDown);
121
124
 
122
125
  const isShowSetupEmergencyContact = useMemo(
@@ -438,21 +441,32 @@ const DeviceDetail = ({ route }) => {
438
441
  // eslint-disable-next-line react-hooks/exhaustive-deps
439
442
  }, [sensor, unit, isNetworkConnected, fetchDataDeviceDetail]);
440
443
 
444
+ const evaluateValue = useEvaluateValue();
445
+
441
446
  const getData = useCallback(
442
447
  (item) => {
443
448
  if (!item.configuration) {
444
449
  return;
445
450
  }
446
451
  const data = item.configuration.configs.map((config) => {
447
- const value = displayValues.find((k) => k.id === config.id);
448
- if (!value) {
452
+ const configValue = configValues[config.id];
453
+ const displayValue = displayValues.find((k) => k.id === config.id);
454
+ if (!configValue && !displayValue) {
449
455
  return;
450
456
  }
457
+ const value = configValue
458
+ ? {
459
+ id: config.id,
460
+ value: configValue,
461
+ evaluate: evaluateValue(config.id, configValue),
462
+ }
463
+ : displayValue;
464
+
451
465
  return { ...config, ...value };
452
466
  });
453
467
  return data.filter((value) => value);
454
468
  },
455
- [displayValues]
469
+ [configValues, displayValues, evaluateValue]
456
470
  );
457
471
 
458
472
  useEffect(() => {
@@ -509,29 +523,6 @@ const DeviceDetail = ({ route }) => {
509
523
  }
510
524
  }, [sensor, display]);
511
525
 
512
- useEffect(() => {
513
- setDisplayValues((currentDisplayValues) => {
514
- for (const [configId, value] of Object.entries(configValues)) {
515
- const intId = parseInt(configId, 10);
516
- const index = currentDisplayValues.findIndex(
517
- (element) => element.id === intId
518
- );
519
-
520
- const item = currentDisplayValues[index];
521
- if (index !== -1) {
522
- currentDisplayValues[index].value = value;
523
- currentDisplayValues[index].evaluate = item.evaluate;
524
- } else {
525
- currentDisplayValues.push({
526
- id: intId,
527
- value: value,
528
- });
529
- }
530
- }
531
- return currentDisplayValues;
532
- });
533
- }, [configValues, setDisplayValues]);
534
-
535
526
  const isShowEmergencyResolve =
536
527
  display.items.filter(
537
528
  (item) =>
@@ -0,0 +1,97 @@
1
+ import { useCallback } from 'react';
2
+ import { useSCContextSelector } from '../../../context';
3
+
4
+ const evaluateRange = (value, configuration) => {
5
+ /*
6
+ configuration: {
7
+ ranges: [
8
+ { start: 0.5, end: 1.5, evaluate: 'On' },
9
+ { start: -0.5, end: 0.49, evaluate: {text: 'Off'} },
10
+ ]
11
+ }
12
+ */
13
+ if (!value) {
14
+ // eslint-disable-next-line no-param-reassign
15
+ value = 0;
16
+ }
17
+
18
+ for (let i = 0; i < configuration?.ranges?.length; i++) {
19
+ const range = configuration.ranges[i];
20
+ if (range.start <= value && value <= range.end) {
21
+ return range.evaluate;
22
+ }
23
+ if (!range.end && range.start <= value) {
24
+ return range.evaluate;
25
+ }
26
+ }
27
+ return { text: value };
28
+ };
29
+
30
+ const evaluateBoolean = (value, configuration) => {
31
+ /*
32
+ configuration: {
33
+ 'on': {
34
+ 'value': 1,
35
+ 'evaluate': {
36
+ 'text': 'On',
37
+ },
38
+ },
39
+ 'off': {
40
+ 'value': 0,
41
+
42
+ 'evaluate': {
43
+ 'text': 'Off',
44
+ },
45
+ },
46
+ }
47
+ */
48
+ if (!value) {
49
+ // eslint-disable-next-line no-param-reassign
50
+ value = 0;
51
+ }
52
+ if (value === configuration.on?.value) {
53
+ return configuration.on.evaluate;
54
+ }
55
+ if (value === configuration.off?.value) {
56
+ return configuration.off.evaluate;
57
+ }
58
+ return { text: value };
59
+ };
60
+
61
+ const valueEvaluationFuncs = {
62
+ range: evaluateRange,
63
+ boolean: evaluateBoolean,
64
+ };
65
+
66
+ export const useEvaluateValue = () => {
67
+ const valueEvaluations = useSCContextSelector((state) => {
68
+ return state.valueEvaluations;
69
+ });
70
+
71
+ const evaluateValue = useCallback(
72
+ (configId, value) => {
73
+ if (value === null || value === undefined) {
74
+ return { text: '--', color: null };
75
+ }
76
+
77
+ const valueEvaluation = valueEvaluations[configId];
78
+ if (!valueEvaluation) {
79
+ return null;
80
+ }
81
+
82
+ const evaluateFunc = valueEvaluationFuncs[valueEvaluation.template];
83
+ if (!evaluateFunc) {
84
+ return null;
85
+ }
86
+
87
+ try {
88
+ return evaluateFunc(value, valueEvaluation.configuration);
89
+ } catch (e) {
90
+ return null;
91
+ }
92
+ },
93
+ [valueEvaluations]
94
+ );
95
+
96
+ return evaluateValue;
97
+ };
@@ -16,14 +16,14 @@ export const useFavoriteDevice = (device) => {
16
16
  const { success } = await axiosPost(
17
17
  API.DEVICE.ADD_TO_FAVOURITES(device?.id)
18
18
  );
19
- success && setAction(Action.ADD_DEVICE_TO_FAVORITES, device.id);
19
+ success && setAction(Action.ADD_DEVICES_TO_FAVORITES, [device.id]);
20
20
  }, [device, setAction]);
21
21
 
22
22
  const removeFromFavorites = useCallback(async () => {
23
23
  const { success } = await axiosPost(
24
24
  API.DEVICE.REMOVE_FROM_FAVOURITES(device?.id)
25
25
  );
26
- success && setAction(Action.REMOVE_DEVICE_FROM_FAVORITES, device.id);
26
+ success && setAction(Action.REMOVE_DEVICES_FROM_FAVORITES, [device.id]);
27
27
  }, [device, setAction]);
28
28
 
29
29
  return {
@@ -34,7 +34,11 @@ import Routes from '../../utils/Route';
34
34
  import { ToastBottomHelper } from '../../utils/Utils';
35
35
  import ItemAutomate from '../../commons/Automate/ItemAutomate';
36
36
  import withPreventDoubleClick from '../../commons/WithPreventDoubleClick';
37
- import { AUTOMATE_SELECT, AUTOMATE_TYPE } from '../../configs/Constants';
37
+ import {
38
+ AUTOMATE_SELECT,
39
+ AUTOMATE_TYPE,
40
+ STATE_VALUE_SENSOR_TYPES,
41
+ } from '../../configs/Constants';
38
42
  import { popAction } from '../../navigations/utils';
39
43
  import { TESTID } from '../../configs/Constants';
40
44
  import useKeyboardAnimated from '../../hooks/Explore/useKeyboardAnimated';
@@ -359,17 +363,23 @@ const ScriptDetail = ({ route }) => {
359
363
  date_repeat,
360
364
  time_repeat,
361
365
  weekday_repeat,
366
+ sensor_type,
362
367
  } = automate;
363
368
  if (type === AUTOMATE_TYPE.VALUE_CHANGE) {
369
+ const stateConditionData = STATE_VALUE_SENSOR_TYPES.find(
370
+ (i) => i.type === sensor_type
371
+ );
372
+ const isNumberValue = !stateConditionData;
373
+
364
374
  let text;
365
375
  if (condition === '>') {
366
376
  text = 'higher_than';
367
377
  } else if (condition === '<') {
368
378
  text = 'lower_than';
369
379
  } else if (condition === '=') {
370
- text = 'equal';
380
+ text = isNumberValue ? 'equal' : stateConditionData?.stateValue[value];
371
381
  }
372
- return `${config_name} ${t(text)} ${value}`;
382
+ return `${config_name} ${t(text)} ${isNumberValue ? value : ''}`;
373
383
  } else if (type === AUTOMATE_TYPE.SCHEDULE) {
374
384
  const time =
375
385
  time_repeat?.length >= 8
@@ -4,8 +4,9 @@ import React, {
4
4
  useState,
5
5
  useRef,
6
6
  useContext,
7
+ useMemo,
7
8
  } from 'react';
8
- import { AppState, RefreshControl, View } from 'react-native';
9
+ import { AppState, RefreshControl, View, Platform } from 'react-native';
9
10
  import { useIsFocused } from '@react-navigation/native';
10
11
 
11
12
  import { useTranslations } from '../../hooks/Common/useTranslations';
@@ -24,6 +25,7 @@ import {
24
25
  } from '../../hooks/Common';
25
26
  import { useFavorites } from './hook/useFavorites';
26
27
  import { useUnitConnectRemoteDevices } from './hook/useUnitConnectRemoteDevices';
28
+ import { useValueEvaluations } from '../../hooks/IoT';
27
29
  import { fetchWithCache, axiosGet } from '../../utils/Apis/axios';
28
30
  import ShortDetailSubUnit from '../../commons/SubUnit/ShortDetail';
29
31
  import NavBar from '../../commons/NavBar';
@@ -52,6 +54,7 @@ import MediaPlayerDetail from '../../commons/MediaPlayerDetail';
52
54
  const UnitDetail = ({ route }) => {
53
55
  const t = useTranslations();
54
56
  const { setAction } = useContext(SCContext);
57
+ const isIOS = useMemo(() => Platform.OS === 'ios', []);
55
58
 
56
59
  const {
57
60
  unitId,
@@ -190,6 +193,8 @@ const UnitDetail = ({ route }) => {
190
193
  };
191
194
  }, [fetchDetails]);
192
195
 
196
+ useValueEvaluations(unitId);
197
+
193
198
  useUnitConnectRemoteDevices(unit);
194
199
 
195
200
  useEffect(() => {
@@ -292,13 +297,13 @@ const UnitDetail = ({ route }) => {
292
297
  }, [user, onRefresh]);
293
298
 
294
299
  useEffect(() => {
295
- if (isFirstOpenCamera) {
300
+ if (isFirstOpenCamera && isIOS) {
296
301
  const to = setTimeout(() => {
297
302
  setAction(Action.IS_FIRST_OPEN_CAMERA, false);
298
303
  clearTimeout(to);
299
304
  }, 3000);
300
305
  }
301
- }, [isFirstOpenCamera, setAction]);
306
+ }, [isFirstOpenCamera, isIOS, setAction]);
302
307
 
303
308
  return (
304
309
  <WrapParallaxScrollView
@@ -315,7 +320,7 @@ const UnitDetail = ({ route }) => {
315
320
  onBack={(isSuccessfullyConnected && Dashboard) || (routeName && onBack)}
316
321
  >
317
322
  {/* NOTE: This is a trick to fix camera not full screen on first open app */}
318
- {isFirstOpenCamera && (
323
+ {isFirstOpenCamera && isIOS && (
319
324
  <MediaPlayerDetail
320
325
  uri={Constants.URL_STREAM_CAMERA_DEMO}
321
326
  isPaused={false}
@@ -70,9 +70,7 @@ const SelectAddress = memo(({ route }) => {
70
70
  API.EXTERNAL.GOOGLE_MAP.AUTO_COMPLETE,
71
71
  config
72
72
  );
73
- if (success) {
74
- setSearchData(data.predictions);
75
- }
73
+ success && setSearchData(data.predictions);
76
74
  // eslint-disable-next-line no-empty
77
75
  } catch (error) {}
78
76
  }, []);
@@ -0,0 +1,158 @@
1
+ import React, {
2
+ memo,
3
+ useState,
4
+ useEffect,
5
+ useCallback,
6
+ useMemo,
7
+ useContext,
8
+ } from 'react';
9
+ import { View, ScrollView, TouchableOpacity } from 'react-native';
10
+ import { useNavigation } from '@react-navigation/native';
11
+ import { Icon } from '@ant-design/react-native';
12
+ import { HeaderCustom } from '../../commons/Header';
13
+ import Text from '../../commons/Text';
14
+ import NavBar from '../../commons/NavBar';
15
+ import BottomButtonView from '../../commons/BottomButtonView';
16
+ import { FullLoading } from '../../commons';
17
+ import Device from '../AddNewAction/Device';
18
+ import { useTranslations } from '../../hooks/Common/useTranslations';
19
+ import { SCContext } from '../../context';
20
+ import { Action } from '../../context/actionType';
21
+ import { axiosGet, axiosPost } from '../../utils/Apis/axios';
22
+ import { API, Colors } from '../../configs';
23
+ import styles from './SelectDevicesStyles';
24
+
25
+ const SelectDevices = memo(({ route }) => {
26
+ const t = useTranslations();
27
+ const { goBack } = useNavigation();
28
+ const { unitId } = route.params;
29
+ const { setAction } = useContext(SCContext);
30
+ const [listStation, setListStation] = useState([]);
31
+ const [listMenuItem, setListMenuItem] = useState([]);
32
+ const [indexStation, setIndexStation] = useState(0);
33
+ const [stations, setStations] = useState([]);
34
+ const [selectedIds, setSelectedIds] = useState([]);
35
+ const [loading, setLoading] = useState(false);
36
+
37
+ const fetchData = useCallback(async () => {
38
+ setLoading(true);
39
+ const { success, data } = await axiosGet(API.UNIT.DEVICES(unitId));
40
+ if (success) {
41
+ const newData = data.filter((item) => item.devices.length > 0);
42
+ const listMenu = newData.map((item, index) => ({
43
+ text: item.name,
44
+ station: item,
45
+ index: index,
46
+ }));
47
+ setStations(newData);
48
+ setListMenuItem(listMenu);
49
+ setListStation(listMenu);
50
+ }
51
+ setLoading(false);
52
+ }, [unitId]);
53
+
54
+ const addDevicesToFavorites = useCallback(async () => {
55
+ if (selectedIds.length === 0) {
56
+ return;
57
+ }
58
+ setLoading(true);
59
+ const { success } = await axiosPost(
60
+ API.UNIT.ADD_DEVICES_TO_FAVORITES(unitId),
61
+ {
62
+ devices: selectedIds,
63
+ }
64
+ );
65
+ if (success) {
66
+ setAction(Action.ADD_DEVICES_TO_FAVORITES, selectedIds);
67
+ goBack();
68
+ }
69
+ setLoading(false);
70
+ }, [unitId, selectedIds, setAction, goBack]);
71
+
72
+ useEffect(() => {
73
+ fetchData();
74
+ }, [fetchData]);
75
+
76
+ const onSnapToItem = useCallback(
77
+ (item, index) => {
78
+ setIndexStation(index);
79
+ },
80
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
+ [unitId, indexStation]
82
+ );
83
+
84
+ const onSelectDevice = useCallback(
85
+ (device) => {
86
+ setSelectedIds((ids) => {
87
+ const index = ids.indexOf(device.id);
88
+ if (index !== -1) {
89
+ return ids.filter((id) => id !== device.id);
90
+ }
91
+ return [...ids, device.id];
92
+ });
93
+ },
94
+ [setSelectedIds]
95
+ );
96
+
97
+ const rightComponent = useMemo(
98
+ () => (
99
+ <TouchableOpacity style={styles.buttonClose} onPress={goBack}>
100
+ <Icon name={'close'} size={24} color={Colors.Black} />
101
+ </TouchableOpacity>
102
+ ),
103
+ // eslint-disable-next-line react-hooks/exhaustive-deps
104
+ []
105
+ );
106
+
107
+ return (
108
+ <View style={styles.wrap}>
109
+ <HeaderCustom rightComponent={rightComponent} />
110
+ <ScrollView
111
+ style={styles.wrap}
112
+ contentContainerStyle={styles.contentContainerStyle}
113
+ scrollIndicatorInsets={{ right: 1 }}
114
+ >
115
+ <Text bold type="H2" style={styles.title}>
116
+ {t('select_device')}
117
+ </Text>
118
+
119
+ {listStation.length ? (
120
+ <NavBar
121
+ listStation={listStation}
122
+ listMenuItem={listMenuItem}
123
+ onSnapToItem={onSnapToItem}
124
+ indexStation={indexStation}
125
+ style={styles.navbar}
126
+ />
127
+ ) : (
128
+ <View style={styles.noneData}>
129
+ <Text center>{t('text_unit_add_to_favorites_no_devices')}</Text>
130
+ </View>
131
+ )}
132
+
133
+ <View style={styles.boxDevices}>
134
+ {stations[indexStation]?.devices &&
135
+ stations[indexStation].devices.map((device) => (
136
+ <Device
137
+ svgMain={device.icon || 'sensor'}
138
+ title={device.name}
139
+ sensor={device}
140
+ isSelectDevice={selectedIds.includes(device.id)}
141
+ onPress={onSelectDevice}
142
+ />
143
+ ))}
144
+ </View>
145
+ </ScrollView>
146
+
147
+ <BottomButtonView
148
+ style={styles.bottomButtonView}
149
+ mainTitle={t('done')}
150
+ onPressMain={addDevicesToFavorites}
151
+ typeMain={selectedIds.length === 0 ? 'disabled' : 'primary'}
152
+ />
153
+ {loading && <FullLoading />}
154
+ </View>
155
+ );
156
+ });
157
+
158
+ export default SelectDevices;
@@ -0,0 +1,40 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { getBottomSpace } from 'react-native-iphone-x-helper';
3
+ import { Colors, Constants } from '../../configs';
4
+
5
+ export default StyleSheet.create({
6
+ wrap: {
7
+ flex: 1,
8
+ backgroundColor: Colors.White,
9
+ },
10
+ contentContainerStyle: {
11
+ paddingBottom: getBottomSpace() + 100,
12
+ },
13
+ navbar: {
14
+ paddingTop: 16,
15
+ },
16
+ title: {
17
+ marginHorizontal: 16,
18
+ },
19
+ boxDevices: {
20
+ flexWrap: 'wrap',
21
+ flexDirection: 'row',
22
+ marginTop: 22,
23
+ justifyContent: 'space-between',
24
+ paddingLeft: 16,
25
+ paddingRight: 16,
26
+ },
27
+
28
+ bottomButtonView: {
29
+ paddingTop: 24,
30
+ paddingBottom: 32,
31
+
32
+ backgroundColor: Colors.White,
33
+ borderColor: Colors.ShadownTransparent,
34
+ borderTopWidth: 1,
35
+ },
36
+ noneData: {
37
+ paddingHorizontal: 16,
38
+ marginTop: Constants.height * 0.3,
39
+ },
40
+ });
@@ -103,6 +103,22 @@ describe('Test SelectAddress', () => {
103
103
  updateLocation: mockUpdateLocation,
104
104
  },
105
105
  };
106
+ mockUpdateLocation.mockClear();
107
+ mockGoBack.mockClear();
108
+ mock.resetHistory();
109
+ });
110
+
111
+ test('test not do anything then click done', async () => {
112
+ await act(async () => {
113
+ tree = await create(wrapComponent(route));
114
+ });
115
+ const instance = tree.root;
116
+ const bottomButton = instance.findByType(BottomButtonView);
117
+ await act(async () => {
118
+ await bottomButton.props.onPressMain();
119
+ });
120
+ expect(mockUpdateLocation).not.toBeCalled();
121
+ expect(mockGoBack).not.toBeCalled();
106
122
  });
107
123
 
108
124
  test('test search location', async () => {
@@ -151,7 +167,6 @@ describe('Test SelectAddress', () => {
151
167
  mock
152
168
  .onGet(API.EXTERNAL.GOOGLE_MAP.GET_LAT_LNG_BY_PLACE_ID)
153
169
  .reply(200, response.data);
154
-
155
170
  await act(async () => {
156
171
  await rowLocations[0].props.onPress({ place_id: 1, description: '1' });
157
172
  });
@@ -163,6 +178,52 @@ describe('Test SelectAddress', () => {
163
178
  expect(mockGoBack).toBeCalled();
164
179
  });
165
180
 
181
+ test('test get lat lng of location failed', async () => {
182
+ await act(async () => {
183
+ tree = await create(wrapComponent(route));
184
+ });
185
+ const instance = tree.root;
186
+ const searchBars = instance.findAllByType(SearchBarLocation);
187
+ expect(searchBars).toHaveLength(1);
188
+
189
+ let response = {
190
+ status: 200,
191
+ data: {
192
+ predictions: [
193
+ { place_id: 1, description: '1' },
194
+ { place_id: 2, description: '2' },
195
+ { place_id: 3, description: '3' },
196
+ ],
197
+ },
198
+ };
199
+ mock.onGet(API.EXTERNAL.GOOGLE_MAP.AUTO_COMPLETE).reply(200, response.data);
200
+ await act(async () => {
201
+ await searchBars[0].props.onTextInput('');
202
+ });
203
+ let rowLocations = instance.findAllByType(RowLocation);
204
+ expect(rowLocations).toHaveLength(0);
205
+
206
+ await act(async () => {
207
+ await searchBars[0].props.onTextInput('input');
208
+ });
209
+ rowLocations = instance.findAllByType(RowLocation);
210
+ expect(rowLocations).toHaveLength(3);
211
+ response = {
212
+ status: 404,
213
+ data: {},
214
+ };
215
+ mock
216
+ .onGet(API.EXTERNAL.GOOGLE_MAP.GET_LAT_LNG_BY_PLACE_ID)
217
+ .reply(404, response.data);
218
+ await act(async () => {
219
+ await rowLocations[0].props.onPress({ place_id: 1, description: '1' });
220
+ });
221
+ const bottomButton = instance.findByType(BottomButtonView);
222
+ await act(async () => {
223
+ await bottomButton.props.onPressMain();
224
+ });
225
+ });
226
+
166
227
  test('test get current location success', async () => {
167
228
  await act(async () => {
168
229
  tree = await create(wrapComponent(route));
@@ -196,6 +257,34 @@ describe('Test SelectAddress', () => {
196
257
  });
197
258
  });
198
259
 
260
+ test('test get current location success get location name failed', async () => {
261
+ await act(async () => {
262
+ tree = await create(wrapComponent(route));
263
+ });
264
+ const instance = tree.root;
265
+ const button = instance.find(
266
+ (el) => el.props.testID === TESTID.BUTTON_YOUR_LOCATION
267
+ );
268
+
269
+ const response = {
270
+ status: 400,
271
+ data: {
272
+ results: [],
273
+ },
274
+ };
275
+ mock
276
+ .onGet(API.EXTERNAL.GOOGLE_MAP.GET_LOCATION_FROM_LAT_LNG)
277
+ .reply(400, response.data);
278
+ await act(async () => {
279
+ await button.props.onPress();
280
+ });
281
+ const bottomButton = instance.findByType(BottomButtonView);
282
+ await act(async () => {
283
+ await bottomButton.props.onPressMain();
284
+ });
285
+ expect(mockGoBack).toBeCalled();
286
+ });
287
+
199
288
  test('test get current location failed permission denied', async () => {
200
289
  await act(async () => {
201
290
  tree = await create(wrapComponent(route));
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import { act, create } from 'react-test-renderer';
3
+ import MockAdapter from 'axios-mock-adapter';
4
+
5
+ import { SCProvider } from '../../../context';
6
+ import { mockSCStore } from '../../../context/mockStore';
7
+ import SelectDevices from '../SelectDevices';
8
+ import Device from '../../AddNewAction/Device';
9
+ import BottomButtonView from '../../../commons/BottomButtonView';
10
+ import { API } from '../../../configs';
11
+ import api from '../../../utils/Apis/axios';
12
+
13
+ const wrapComponent = (route) => (
14
+ <SCProvider initState={mockSCStore({})}>
15
+ <SelectDevices route={route} />
16
+ </SCProvider>
17
+ );
18
+
19
+ const mock = new MockAdapter(api.axiosInstance);
20
+
21
+ const mockGoBack = jest.fn();
22
+ jest.mock('@react-navigation/native', () => {
23
+ return {
24
+ ...jest.requireActual('@react-navigation/native'),
25
+ useNavigation: () => ({
26
+ goBack: mockGoBack,
27
+ }),
28
+ };
29
+ });
30
+
31
+ jest.mock('react', () => {
32
+ return {
33
+ ...jest.requireActual('react'),
34
+ memo: (x) => x,
35
+ };
36
+ });
37
+
38
+ describe('Test SelectDevices', () => {
39
+ let tree, route;
40
+
41
+ beforeAll(() => {
42
+ mockGoBack.mockClear();
43
+ mock.resetHistory();
44
+ route = {
45
+ params: {
46
+ unitId: 1,
47
+ },
48
+ };
49
+ });
50
+
51
+ test('test select then add devices to favorites', async () => {
52
+ let data = [
53
+ {
54
+ id: 1,
55
+ name: 'station 1',
56
+ devices: [
57
+ {
58
+ id: 1,
59
+ name: 'device 1',
60
+ icon: 'sensor',
61
+ icon_kit: null,
62
+ },
63
+ {
64
+ id: 2,
65
+ name: 'device 2',
66
+ icon: null,
67
+ icon_kit: 'icon',
68
+ },
69
+ ],
70
+ },
71
+ {
72
+ id: 2,
73
+ name: 'station 2',
74
+ devices: [],
75
+ },
76
+ ];
77
+ mock.onGet(API.UNIT.DEVICES(1)).replyOnce(200, data);
78
+ mock.onPost(API.UNIT.ADD_DEVICES_TO_FAVORITES(1)).replyOnce(200);
79
+
80
+ await act(async () => {
81
+ tree = await create(wrapComponent(route));
82
+ });
83
+ const instance = tree.root;
84
+
85
+ const bottomButton = instance.findByType(BottomButtonView);
86
+
87
+ await act(async () => {
88
+ await bottomButton.props.onPressMain();
89
+ });
90
+ expect(mock.history.post).toHaveLength(0);
91
+
92
+ const devices = instance.findAllByType(Device);
93
+ expect(devices).toHaveLength(2);
94
+
95
+ await act(async () => {
96
+ await devices[0].props.onPress({ id: 1 });
97
+ });
98
+ await act(async () => {
99
+ await devices[1].props.onPress({ id: 2 });
100
+ });
101
+ await act(async () => {
102
+ await devices[0].props.onPress({ id: 1 });
103
+ });
104
+ await act(async () => {
105
+ await bottomButton.props.onPressMain();
106
+ });
107
+ expect(mock.history.post).toHaveLength(1);
108
+ expect(mockGoBack).toBeCalled();
109
+ });
110
+ });
@@ -5,6 +5,7 @@ import NetInfo from '@react-native-community/netinfo';
5
5
  import { PROBLEM_CODE } from '../../configs/Constants';
6
6
  import { getTranslate } from '../I18n';
7
7
  import { SCConfig } from '../../configs';
8
+ import { isHTML } from '../Validation';
8
9
 
9
10
  const api = create({
10
11
  headers: {
@@ -46,6 +47,11 @@ const parseErrorResponse = async (error) => {
46
47
  case PROBLEM_CODE.SERVER_ERROR:
47
48
  message = getTranslate(SCConfig.language, 'server_error');
48
49
  break;
50
+ case PROBLEM_CODE.CLIENT_ERROR:
51
+ if (error.status === 404 && isHTML(error.data)) {
52
+ message = getTranslate(SCConfig.language, 'not_found');
53
+ }
54
+ break;
49
55
  }
50
56
  ToastBottomHelper.error(message);
51
57
  }
@@ -995,5 +995,7 @@
995
995
  "detected": "Detected",
996
996
  "not_detected": "Not detected",
997
997
  "activated": "Activated",
998
+ "text_unit_add_to_favorites_no_devices": "You don't have any devices or all your devices was added to your favorites",
999
+ "not_found": "Not found",
998
1000
  "not_activated": "Not activated"
999
1001
  }
@@ -996,5 +996,7 @@
996
996
  "detected": "Phát hiện",
997
997
  "not_detected": "Không phát hiện",
998
998
  "activated": "Được kích hoạt",
999
+ "text_unit_add_to_favorites_no_devices": "Bạn không có thiết bị nào hoặc tất cả thiết bị của bạn đã được thêm vào yêu thích",
1000
+ "not_found": "Không tìm thấy",
999
1001
  "not_activated": "Không kích hoạt"
1000
1002
  }
@@ -146,6 +146,7 @@ const Routes = {
146
146
  ItemPasscode: 'ItemPasscode',
147
147
  UnitMemberInformation: 'UnitMemberInformation',
148
148
  EnterPassword: 'EnterPassword',
149
+ SelectDevices: 'SelectDevices',
149
150
  };
150
151
 
151
152
  export default Routes;
@@ -7,3 +7,6 @@ export const isValidPhoneNumber = (phoneNumber) => {
7
7
  export const isValidEmailAddress = (emailAddress) => {
8
8
  return /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(emailAddress);
9
9
  };
10
+
11
+ export const isHTML = (string) =>
12
+ /<[a-z]+\d?(\s+[\w-]+=("[^"]*"|'[^']*'))*\s*\/?>|&#?\w+;/i.test(string);