Skip to content

Commit a3b676f

Browse files
Implemented feature to install firmware via app
1 parent 107ed40 commit a3b676f

File tree

14 files changed

+835
-96
lines changed

14 files changed

+835
-96
lines changed

licenses.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
"licenseUrl": "http://github.com/mwittig/npm-license-crawler/raw/master/LICENSE",
9696
"parents": "opendtu-react-native"
9797
},
98-
98+
9999
"licenses": "MIT",
100100
"repository": "github:octokit/octokit.js",
101101
"licenseUrl": "github:octokit/octokit.js",
@@ -113,6 +113,12 @@
113113
"licenseUrl": "https://github.com/ds300/postinstall-postinstall/raw/master/LICENSE",
114114
"parents": "opendtu-react-native"
115115
},
116+
117+
"licenses": "MIT",
118+
"repository": "https://github.com/sindresorhus/pretty-bytes",
119+
"licenseUrl": "https://github.com/sindresorhus/pretty-bytes/raw/master/license",
120+
"parents": "opendtu-react-native"
121+
},
116122
117123
"licenses": "MIT",
118124
"repository": "https://github.com/samber/prometheus-query-js",
@@ -143,6 +149,12 @@
143149
"licenseUrl": "https://github.com/yamankatby/react-native-flex-layout/raw/master/LICENSE",
144150
"parents": "opendtu-react-native"
145151
},
152+
153+
"licenses": "MIT",
154+
"repository": "https://github.com/itinance/react-native-fs",
155+
"licenseUrl": "https://github.com/itinance/react-native-fs/raw/master/LICENSE",
156+
"parents": "opendtu-react-native"
157+
},
146158
147159
"licenses": "MIT",
148160
"repository": "https://github.com/LinusU/react-native-get-random-values",

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,15 @@
4646
"octokit": "^3.2.0",
4747
"patch-package": "^8.0.0",
4848
"postinstall-postinstall": "^2.1.0",
49+
"pretty-bytes": "^6.1.1",
4950
"prometheus-query": "^3.4.0",
5051
"react": "18.2.0",
5152
"react-i18next": "^14.1.1",
5253
"react-native": "^0.74.1",
5354
"react-native-charts-wrapper": "^0.6.0",
5455
"react-native-fast-image": "^8.6.3",
5556
"react-native-flex-layout": "^0.1.5",
57+
"react-native-fs": "^2.20.0",
5658
"react-native-get-random-values": "^1.11.0",
5759
"react-native-linear-gradient": "^2.8.3",
5860
"react-native-logs": "^5.1.0",

src/api/opendtuapi.ts

Lines changed: 120 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
import type { Index, OpenDTUConfig } from '@/types/settings';
1313

1414
import { rootLogger } from '@/utils/log';
15+
import { downloadFirmware, uploadFirmware } from '@/firmware';
1516

1617
const log = rootLogger.extend('OpenDtuApi');
1718

@@ -75,7 +76,7 @@ class OpenDtuApi {
7576
constructor() {
7677
this.wsId = Math.random().toString(36).substring(2, 9);
7778

78-
log.info('OpenDtuApi.constructor()');
79+
log.debug('OpenDtuApi.constructor()');
7980
}
8081

8182
public setLocale(locale: string): void {
@@ -95,7 +96,7 @@ class OpenDtuApi {
9596
}
9697

9798
public startFetchHttpStateInterval(): void {
98-
log.info('OpenDtuApi.startFetchHttpStateInterval()');
99+
log.debug('OpenDtuApi.startFetchHttpStateInterval()');
99100

100101
if (this.fetchHttpStateInterval) {
101102
clearInterval(this.fetchHttpStateInterval);
@@ -104,7 +105,7 @@ class OpenDtuApi {
104105
this.fetchHttpStateInterval = setInterval(() => {
105106
this.updateHttpState();
106107

107-
log.info(
108+
log.debug(
108109
'interval -> OpenDtuApi.updateHttpState()',
109110
new Date(),
110111
this.index,
@@ -181,8 +182,10 @@ class OpenDtuApi {
181182
controller.abort();
182183
}, 5000);
183184

184-
log.info('getSystemStatusFromUrl', url);
185-
const response = await fetch(`${url.origin}/api/system/status`, {
185+
log.debug('getSystemStatusFromUrl', url);
186+
const path = `${url.origin}/api/system/status`;
187+
188+
const response = await fetch(path, {
186189
signal: controller.signal,
187190
method: 'GET',
188191
headers: {
@@ -296,7 +299,7 @@ class OpenDtuApi {
296299
};
297300

298301
try {
299-
log.info('checkCredentials', baseUrl, requestOptions);
302+
log.debug('checkCredentials', baseUrl, requestOptions);
300303
const response = await fetch(
301304
`${baseUrl}/api/security/authenticate`,
302305
requestOptions,
@@ -317,7 +320,7 @@ class OpenDtuApi {
317320
}
318321
}
319322

320-
public connect(noInterval = false): void {
323+
public connect(): void {
321324
// connect websocket
322325
if (this.ws !== null) {
323326
log.warn('OpenDtuApi.connect() ws not null, aborting!');
@@ -332,17 +335,15 @@ class OpenDtuApi {
332335

333336
const url = `${protocol}://${authString ?? ''}${host}/livedata`;
334337

335-
log.info('OpenDtuApi.connect()', url);
338+
log.debug('OpenDtuApi.connect()', url);
336339

337340
this.ws = new WebSocket(url);
338341

339342
this.ws.onopen = () => {
340343
this.wsUrl = url;
341-
log.info('OpenDtuApi.onopen()');
344+
log.debug('OpenDtuApi.onopen()');
342345

343-
if (!noInterval) {
344-
this.startFetchHttpStateInterval();
345-
}
346+
this.startFetchHttpStateInterval();
346347

347348
this.wsConnected = true;
348349

@@ -352,7 +353,7 @@ class OpenDtuApi {
352353
};
353354

354355
this.ws.onmessage = evt => {
355-
log.info('OpenDtuApi.onmessage()');
356+
log.debug('OpenDtuApi.onmessage()');
356357

357358
let parsedData: LiveDataFromWebsocket | null = null;
358359

@@ -406,20 +407,20 @@ class OpenDtuApi {
406407
}
407408

408409
setTimeout(() => {
409-
log.debug('Reconnecting websocket', {
410+
log.info('Reconnecting websocket', {
410411
wsHost: host,
411412
currentUrl: this.baseUrl,
412413
wsId: this.wsId,
413414
});
414-
this.connect(true);
415+
this.connect();
415416
}, 1000);
416417
};
417418

418419
this.ws.onerror = evt => {
419420
log.error('OpenDtuApi.onerror()', evt.message);
420421
};
421422

422-
log.info('OpenDtuApi.connect()', url);
423+
log.debug('OpenDtuApi.connect()', url);
423424
}
424425
}
425426

@@ -472,15 +473,15 @@ class OpenDtuApi {
472473
}
473474

474475
public setConfig(config: OpenDTUConfig, index: Index): void {
475-
log.info('OpenDtuApi.setConfig()', { config, index });
476+
log.debug('OpenDtuApi.setConfig()', { config, index });
476477

477478
this.setBaseUrl(config.baseUrl);
478479
this.setUserString(config.userString);
479480
this.setIndex(index);
480481
}
481482

482483
public async updateHttpState(): Promise<void> {
483-
log.info('OpenDtuApi.updateHttpState()');
484+
log.debug('OpenDtuApi.updateHttpState()');
484485

485486
if (this.index === null) {
486487
log.warn('OpenDtuApi.updateHttpState() index is null');
@@ -704,6 +705,107 @@ class OpenDtuApi {
704705
wsReadyState: this.ws?.readyState ?? 'undefined',
705706
};
706707
}
708+
709+
public async downloadOTA(
710+
version: string,
711+
downloadUrl: string,
712+
onDownloadProgressEvent: (progress: number) => void,
713+
): Promise<string | null> {
714+
return await downloadFirmware(
715+
version,
716+
downloadUrl,
717+
onDownloadProgressEvent,
718+
);
719+
}
720+
721+
public async handleOTA(
722+
version: string,
723+
path: string | null,
724+
onUploadProgressEvent: (progress: number) => void,
725+
): Promise<boolean> {
726+
if (!path) {
727+
log.error('handleOTA', 'download failed');
728+
return false;
729+
}
730+
731+
const headers = {
732+
...(this.userString ? { Authorization: 'Basic ' + this.userString } : {}),
733+
};
734+
735+
const authString = this.getAuthString();
736+
737+
const url = `${authString ?? ''}${this.baseUrl}/api/firmware/update`;
738+
739+
const res = await uploadFirmware(
740+
version,
741+
path,
742+
url,
743+
headers,
744+
onUploadProgressEvent,
745+
);
746+
747+
return res?.statusCode === 200;
748+
}
749+
750+
public awaitForUpdateFinish(): Promise<void> {
751+
return new Promise((resolve, reject) => {
752+
// fetch from /api/system/status using HTTP HEAD. if okay, resolve. after 1 minute, reject.
753+
let fetchInterval: NodeJS.Timeout | null = null;
754+
755+
const rejectTimeout = setTimeout(() => {
756+
log.warn('waiting took too long');
757+
758+
if (fetchInterval) {
759+
clearInterval(fetchInterval);
760+
}
761+
762+
reject();
763+
}, 60 * 1000);
764+
765+
const authString = this.getAuthString();
766+
767+
const url = `${authString ?? ''}${this.baseUrl}/api/system/status`;
768+
769+
const execFetch = () => {
770+
const controller = new AbortController();
771+
772+
const requestOptions = {
773+
method: 'HEAD',
774+
signal: controller.signal,
775+
headers: {
776+
'X-Requested-With': 'XMLHttpRequest',
777+
'Content-Type': 'application/json',
778+
...(this.userString
779+
? { Authorization: 'Basic ' + this.userString }
780+
: {}),
781+
},
782+
};
783+
784+
const abortTimeout = setTimeout(() => {
785+
controller.abort();
786+
}, 1000 * 3);
787+
788+
fetch(url, requestOptions)
789+
.then(response => {
790+
if (response.status === 200) {
791+
clearTimeout(abortTimeout);
792+
clearTimeout(rejectTimeout);
793+
794+
if (fetchInterval) {
795+
clearInterval(fetchInterval);
796+
}
797+
798+
resolve();
799+
}
800+
})
801+
.catch(() => {});
802+
};
803+
804+
fetchInterval = setInterval(() => {
805+
execFetch();
806+
}, 3000);
807+
});
808+
}
707809
}
708810

709811
export type DebugInfo = ReturnType<OpenDtuApi['getDebugInfo']>;

src/components/BaseModal.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@ import type { FC } from 'react';
22
import type { ModalProps } from 'react-native-paper';
33
import { Modal, useTheme } from 'react-native-paper';
44

5-
const BaseModal: FC<ModalProps> = ({ children, ...rest }) => {
5+
export interface BaseModalProps extends ModalProps {
6+
backgroundColor?: string;
7+
}
8+
9+
const BaseModal: FC<BaseModalProps> = ({
10+
children,
11+
backgroundColor,
12+
...rest
13+
}) => {
614
const theme = useTheme();
715
return (
816
<Modal
917
{...rest}
1018
contentContainerStyle={{
11-
backgroundColor: theme.colors.elevation.level4,
19+
backgroundColor: backgroundColor ?? theme.colors.elevation.level4,
1220
padding: 8,
1321
borderRadius: 28,
1422
marginVertical: 8,

0 commit comments

Comments
 (0)