Skip to content

Commit f6b730a

Browse files
Merge pull request #162 from OpenDTU-App/29-implement-network-settings
Implement network settings
2 parents 52fc9a1 + 2f556f9 commit f6b730a

39 files changed

+1216
-61
lines changed

licenses.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@
7777
"licenseUrl": "https://github.com/expo/expo",
7878
"parents": "opendtu-react-native"
7979
},
80+
81+
"licenses": "MIT",
82+
"repository": "https://github.com/epoberezkin/fast-deep-equal",
83+
"licenseUrl": "https://github.com/epoberezkin/fast-deep-equal/raw/master/LICENSE",
84+
"parents": "opendtu-react-native"
85+
},
8086
8187
"licenses": "MIT",
8288
"repository": "https://github.com/i18next/i18next",
@@ -149,6 +155,12 @@
149155
"licenseUrl": "https://github.com/i18next/react-i18next/raw/master/LICENSE",
150156
"parents": "opendtu-react-native"
151157
},
158+
159+
"licenses": "ISC",
160+
"repository": "https://github.com/AtharvaDeolalikar/react-native-animated-bottom-drawer",
161+
"licenseUrl": "https://github.com/AtharvaDeolalikar/react-native-animated-bottom-drawer",
162+
"parents": "opendtu-react-native"
163+
},
152164
153165
"licenses": "MIT",
154166
"repository": "https://github.com/wuxudong/react-native-charts-wrapper",

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@
4444
"base-64": "^1.0.0",
4545
"compare-versions": "^6.1.1",
4646
"expo": "^51.0.23",
47+
"fast-deep-equal": "^3.1.3",
4748
"i18next": "^23.12.2",
4849
"intl-pluralrules": "^2.0.1",
4950
"ip-regex": "^5.0.0",
5051
"moment": "^2.30.1",
51-
"node-git-hooks": "^1.0.7",
52+
"node-git-hooks": "1.0.7",
5253
"npm-license-crawler": "^0.2.1",
5354
"octokit": "^3.2.1",
5455
"patch-package": "^8.0.0",
@@ -58,6 +59,7 @@
5859
"react": "18.3.1",
5960
"react-i18next": "^15.0.0",
6061
"react-native": "0.74.3",
62+
"react-native-animated-bottom-drawer": "^0.0.23",
6163
"react-native-charts-wrapper": "^0.6.0",
6264
"react-native-fast-image": "^8.6.3",
6365
"react-native-flex-layout": "^0.1.5",

src/api/ApiHandler.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
setLiveDataFromStatus,
1414
setLiveDataFromWebsocket,
1515
setMqttStatus,
16+
setNetworkSettings,
1617
setNetworkStatus,
1718
setNtpStatus,
1819
setPowerStatus,
@@ -176,6 +177,10 @@ export const ApiProvider: FC<PropsWithChildren> = ({ children }) => {
176177
dispatch(setGridProfile({ data, index, inverterSerial }));
177178
});
178179

180+
api.registerOnNetworkSettingsHandler((data, index) => {
181+
dispatch(setNetworkSettings({ data, index }));
182+
});
183+
179184
log.debug('Connecting API Handler');
180185

181186
api.connect();

src/api/opendtuapi.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
import type { EventLogData } from '@/types/opendtu/eventlog';
1010
import type { GridProfileData } from '@/types/opendtu/gridprofile';
1111
import type { InverterDeviceData } from '@/types/opendtu/inverterDevice';
12+
import type { NetworkSettings } from '@/types/opendtu/settings';
1213
import type { InverterItem } from '@/types/opendtu/state';
1314
import { DeviceState } from '@/types/opendtu/state';
1415
import type {
@@ -99,6 +100,9 @@ class OpenDtuApi {
99100
inverterSerial: InverterSerial,
100101
) => void)
101102
| null = null;
103+
private onNetworkSettingsHandler:
104+
| ((data: NetworkSettings, index: Index) => void)
105+
| null = null;
102106

103107
private ws: WebSocket | null = null;
104108
// communication
@@ -281,6 +285,16 @@ class OpenDtuApi {
281285
this.onGridProfileHandler = null;
282286
}
283287

288+
public registerOnNetworkSettingsHandler(
289+
handler: (data: NetworkSettings, index: Index) => void,
290+
): void {
291+
this.onNetworkSettingsHandler = handler;
292+
}
293+
294+
public unregisterOnNetworkSettingsHandler(): void {
295+
this.onNetworkSettingsHandler = null;
296+
}
297+
284298
public async getSystemStatusFromUrl(
285299
url: URL,
286300
): Promise<GetSystemStatusReturn> {
@@ -1007,6 +1021,64 @@ class OpenDtuApi {
10071021
return res.status === 200 && parsed.type === 'success';
10081022
}
10091023

1024+
public async getNetworkConfig(): Promise<NetworkSettings | null> {
1025+
if (!this.baseUrl) {
1026+
return null;
1027+
}
1028+
1029+
const res = await this.makeAuthenticatedRequest(
1030+
'/api/network/config',
1031+
'GET',
1032+
);
1033+
1034+
if (!res) {
1035+
log.error('getNetworkConfig', 'no response');
1036+
return null;
1037+
}
1038+
1039+
if (res.status === 200) {
1040+
const json = await res.json();
1041+
1042+
if (this.onNetworkSettingsHandler && this.index !== null) {
1043+
this.onNetworkSettingsHandler(json, this.index);
1044+
}
1045+
1046+
return json;
1047+
}
1048+
1049+
log.error('getNetworkConfig', 'invalid status');
1050+
1051+
return null;
1052+
}
1053+
1054+
public async setNetworkConfig(
1055+
config: NetworkSettings,
1056+
): Promise<boolean | null> {
1057+
if (!this.baseUrl) {
1058+
return null;
1059+
}
1060+
1061+
const formData = new FormData();
1062+
formData.append('data', JSON.stringify(config));
1063+
1064+
const res = await this.makeAuthenticatedRequest(
1065+
'/api/network/config',
1066+
'POST',
1067+
{
1068+
body: formData,
1069+
},
1070+
);
1071+
1072+
if (!res) {
1073+
log.error('setNetworkConfig', 'no response');
1074+
return null;
1075+
}
1076+
1077+
const parsed = await res.json();
1078+
1079+
return res.status === 200 && parsed.type === 'success';
1080+
}
1081+
10101082
public async makeAuthenticatedRequest(
10111083
route: string,
10121084
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PATCH',
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import type { FC } from 'react';
2+
import { useEffect, useRef, useState } from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
import type { BottomDrawerMethods } from 'react-native-animated-bottom-drawer';
5+
import BottomDrawer from 'react-native-animated-bottom-drawer';
6+
import type { SwitchProps } from 'react-native-paper';
7+
import { Button, Switch, Text, useTheme } from 'react-native-paper';
8+
9+
import { View } from 'react-native';
10+
11+
export interface ChangeValueModalProps {
12+
isOpen?: boolean;
13+
onClose?: () => void;
14+
defaultValue?: boolean;
15+
onChange?: (value: boolean) => void;
16+
title?: string;
17+
description?: string;
18+
extraHeight?: number;
19+
inputProps?: Omit<SwitchProps, 'value' | 'onValueChange'>;
20+
}
21+
22+
const ChangeTextValueModal: FC<ChangeValueModalProps> = ({
23+
isOpen,
24+
title,
25+
description,
26+
defaultValue,
27+
onChange: onSave,
28+
onClose,
29+
inputProps,
30+
extraHeight,
31+
}) => {
32+
const theme = useTheme();
33+
const drawerRef = useRef<BottomDrawerMethods>(null);
34+
const { t } = useTranslation();
35+
36+
const height = 180 + (extraHeight ?? 0);
37+
38+
const [value, setValue] = useState<boolean>(defaultValue ?? false);
39+
const [wasModified, setWasModified] = useState<boolean>(false);
40+
41+
useEffect(() => {
42+
if (isOpen) {
43+
drawerRef.current?.open();
44+
} else {
45+
drawerRef.current?.close();
46+
}
47+
}, [isOpen]);
48+
49+
const handleSave = () => {
50+
onSave?.(value);
51+
onClose?.();
52+
};
53+
54+
const handleCancel = () => {
55+
if (!isOpen) {
56+
return;
57+
}
58+
59+
setWasModified(false);
60+
setValue(defaultValue ?? false);
61+
onClose?.();
62+
};
63+
64+
useEffect(() => {
65+
if (!wasModified) {
66+
setValue(defaultValue ?? false);
67+
}
68+
}, [defaultValue, wasModified]);
69+
70+
return (
71+
<BottomDrawer
72+
ref={drawerRef}
73+
overDrag
74+
customStyles={{
75+
container: {
76+
backgroundColor: theme.colors.surface,
77+
},
78+
handleContainer: {
79+
backgroundColor: theme.colors.surfaceVariant,
80+
minHeight: 35,
81+
borderTopLeftRadius: 20,
82+
borderTopRightRadius: 20,
83+
marginBottom: 16,
84+
},
85+
handle: {
86+
backgroundColor: theme.colors.surfaceDisabled,
87+
width: 40,
88+
height: 5,
89+
borderRadius: 5,
90+
},
91+
}}
92+
initialHeight={height}
93+
onClose={handleCancel}
94+
>
95+
<View
96+
style={{
97+
flex: 1,
98+
paddingHorizontal: 14,
99+
}}
100+
>
101+
<View
102+
style={{
103+
display: 'flex',
104+
flexDirection: 'row',
105+
alignItems: 'center',
106+
justifyContent: 'space-between',
107+
}}
108+
>
109+
<View>
110+
<Text variant="titleLarge">{title}</Text>
111+
<Text variant="bodyMedium">{description}</Text>
112+
</View>
113+
<View>
114+
<Switch
115+
value={value}
116+
onValueChange={value => {
117+
setWasModified(true);
118+
setValue(value);
119+
}}
120+
{...inputProps}
121+
/>
122+
</View>
123+
</View>
124+
<View
125+
style={{
126+
flexDirection: 'row',
127+
justifyContent: 'space-between',
128+
gap: 8,
129+
marginTop: 24,
130+
}}
131+
>
132+
<Button mode="text" onPress={handleCancel} style={{ flex: 1 }}>
133+
{t('cancel')}
134+
</Button>
135+
<Button mode="contained" onPress={handleSave} style={{ flex: 1 }}>
136+
{t('save')}
137+
</Button>
138+
</View>
139+
</View>
140+
</BottomDrawer>
141+
);
142+
};
143+
144+
export default ChangeTextValueModal;

src/components/modals/ChangeOpendtuCredentialsModal.tsx

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
33
import { useTranslation } from 'react-i18next';
44
import { Box } from 'react-native-flex-layout';
55
import type { ModalProps } from 'react-native-paper';
6-
import { Button, Portal, Text, useTheme } from 'react-native-paper';
6+
import { Button, HelperText, Portal, Text, useTheme } from 'react-native-paper';
77

88
import { updateDtuUserString } from '@/slices/settings';
99

@@ -111,29 +111,27 @@ const ChangeOpendtuCredentialsModal: FC<
111111
{t('settings.changeOpendtuCredentials')}
112112
</Text>
113113
</Box>
114-
<StyledTextInput
115-
label={t('setup.username')}
116-
mode="outlined"
117-
value={username}
118-
onChangeText={setUsername}
119-
textContentType="username"
120-
style={{ backgroundColor: theme.colors.elevation.level3 }}
121-
/>
122-
<StyledTextInput
123-
label={t('setup.password')}
124-
mode="outlined"
125-
value={password}
126-
onChangeText={setPassword}
127-
textContentType="password"
128-
style={{ backgroundColor: theme.colors.elevation.level3 }}
129-
/>
130-
{error ? (
131-
<Box mt={8}>
132-
<Text variant="bodySmall" style={{ color: theme.colors.error }}>
133-
{error}
134-
</Text>
135-
</Box>
136-
) : null}
114+
<Box mb={8}>
115+
<StyledTextInput
116+
label={t('setup.username')}
117+
mode="outlined"
118+
value={username}
119+
onChangeText={setUsername}
120+
textContentType="username"
121+
style={{ backgroundColor: theme.colors.elevation.level3 }}
122+
/>
123+
<StyledTextInput
124+
label={t('setup.password')}
125+
mode="outlined"
126+
value={password}
127+
onChangeText={setPassword}
128+
textContentType="password"
129+
style={{ backgroundColor: theme.colors.elevation.level3 }}
130+
/>
131+
</Box>
132+
<HelperText type="error" visible={!!error}>
133+
{error}
134+
</HelperText>
137135
</Box>
138136
<Box
139137
style={{

0 commit comments

Comments
 (0)