├── .watchmanconfig
├── assets
├── images
│ ├── icon.png
│ ├── splash.png
│ └── Raspberry.png
└── fonts
│ └── SpaceMono-Regular.ttf
├── .gitignore
├── babel.config.js
├── .prettierrc
├── .eslintrc.js
├── hooks
├── UseColorScheme.web.ts
├── useColorScheme.ts
└── useCachedResources.ts
├── components
├── PiLogo.js
├── StyledText.js
├── TabBarIcon.js
├── Level.js
├── SettingsButton.js
├── Gauge.js
└── EditSettings.js
├── constants
├── Layout.js
└── Colors.js
├── tsconfig.json
├── README.md
├── screens
├── LoadScreen.js
├── PanelScreen.js
├── BatteryScreen.js
├── ChargeScreen.js
└── NotFoundScreen.tsx
├── types.tsx
├── store
├── AppContext.js
└── AppState.js
├── app.json
├── navigation
├── LinkingConfiguration.ts
├── index.tsx
└── BottomTabNavigator.tsx
├── App.tsx
├── LICENSE
├── python
├── solar_test.py
├── solar-rest.py
└── xxv_tracer3.py
├── api
└── registerForPushNotificationsAsync.js
└── package.json
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rdagger/Expo-Solar-Tracker/HEAD/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rdagger/Expo-Solar-Tracker/HEAD/assets/images/splash.png
--------------------------------------------------------------------------------
/assets/images/Raspberry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rdagger/Expo-Solar-Tracker/HEAD/assets/images/Raspberry.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p12
6 | *.key
7 | *.mobileprovision
8 | package-lock.json
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rdagger/Expo-Solar-Tracker/HEAD/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "jsxBracketSameLine": true,
6 | "trailingComma": "es5"
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'universe/native',
3 | rules: {
4 | 'prettier/prettier': [
5 | 'error',
6 | {
7 | endOfLine: 'auto',
8 | },
9 | ],
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/hooks/UseColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | // useColorScheme from react-native does not support web currently. You can replace
2 | // this with react-native-appearance if you would like theme support on web.
3 | export default function useColorScheme() {
4 | return 'light';
5 | }
6 |
--------------------------------------------------------------------------------
/components/PiLogo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Image } from 'react-native';
3 |
4 | export const PiLogo = (props) => {
5 | return (
6 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/constants/Layout.js:
--------------------------------------------------------------------------------
1 | import { Dimensions } from 'react-native';
2 |
3 | const width = Dimensions.get('window').width;
4 | const height = Dimensions.get('window').height;
5 |
6 | export default {
7 | window: {
8 | width,
9 | height,
10 | },
11 | isSmallDevice: width < 375,
12 | };
13 |
--------------------------------------------------------------------------------
/components/StyledText.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text } from 'react-native';
3 |
4 | export class MonoText extends React.Component {
5 | render() {
6 | return (
7 |
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/constants/Colors.js:
--------------------------------------------------------------------------------
1 | const tintColor = '#2f95dc';
2 |
3 | export default {
4 | tintColor,
5 | tabIconDefault: '#ccc',
6 | tabIconSelected: tintColor,
7 | tabBar: '#fefefe',
8 | errorBackground: 'red',
9 | errorText: '#fff',
10 | warningBackground: '#EAEB5E',
11 | warningText: '#666804',
12 | noticeBackground: tintColor,
13 | noticeText: '#fff',
14 | };
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "emitDecoratorMetadata": true,
5 | "experimentalDecorators": true,
6 | "jsx": "react-native",
7 | "lib": ["dom", "esnext"],
8 | "moduleResolution": "node",
9 | "noEmit": true,
10 | "skipLibCheck": true,
11 | "resolveJsonModule": true,
12 | "strict": true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Expo-Solar-Tracker
2 | React native app created with Expo to display photo-voltaic data from a Raspberry Pi Flask REST API.
3 |
4 | Tutorial on my website [Rototron](https://www.rototron.info/raspberry-pi-solar-serial-rest-api-tutorial/) or click picture below for a YouTube video:
5 |
6 | [](https://youtu.be/1VxP38XlVEQ)
7 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ColorSchemeName,
3 | useColorScheme as _useColorScheme,
4 | } from 'react-native';
5 |
6 | // The useColorScheme value is always either light or dark, but the built-in
7 | // type suggests that it can be null. This will not happen in practice, so this
8 | // makes it a bit easier to work with.
9 | export default function useColorScheme(): NonNullable {
10 | return _useColorScheme() as NonNullable;
11 | }
12 |
--------------------------------------------------------------------------------
/components/TabBarIcon.js:
--------------------------------------------------------------------------------
1 | import { Ionicons } from '@expo/vector-icons';
2 | import React from 'react';
3 |
4 | import Colors from '../constants/Colors';
5 |
6 | export default class TabBarIcon extends React.Component {
7 | render() {
8 | return (
9 |
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/screens/LoadScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ScrollView, StyleSheet } from 'react-native';
3 |
4 | import Level from '../components/Level';
5 |
6 | export default function LoadScreen() {
7 | return (
8 |
9 |
10 |
11 | ); // End Load Screen
12 | }
13 |
14 | const styles = StyleSheet.create({
15 | container: {
16 | flex: 1,
17 | paddingTop: 15,
18 | backgroundColor: '#fff',
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/types.tsx:
--------------------------------------------------------------------------------
1 | export type RootStackParamList = {
2 | Root: undefined;
3 | NotFound: undefined;
4 | };
5 |
6 | export type BottomTabParamList = {
7 | Panel: undefined;
8 | Battery: undefined;
9 | Load: undefined;
10 | Charge: undefined;
11 | };
12 |
13 | export type PanelParamList = {
14 | PanelScreen: undefined;
15 | };
16 |
17 | export type BatteryParamList = {
18 | BatteryScreen: undefined;
19 | };
20 | export type LoadParamList = {
21 | LoadScreen: undefined;
22 | };
23 |
24 | export type ChargeParamList = {
25 | ChargeScreen: undefined;
26 | };
27 |
--------------------------------------------------------------------------------
/screens/PanelScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ScrollView, StyleSheet } from 'react-native';
3 |
4 | import Level from '../components/Level';
5 |
6 | export default function PanelScreen() {
7 | return (
8 |
9 |
16 |
17 | ); // End PanelScreen
18 | }
19 |
20 | const styles = StyleSheet.create({
21 | container: {
22 | flex: 1,
23 | paddingTop: 15,
24 | backgroundColor: '#fff',
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/screens/BatteryScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ScrollView, StyleSheet } from 'react-native';
3 |
4 | import Level from '../components/Level';
5 |
6 | export default function BatteryScreen() {
7 | return (
8 |
9 |
16 |
17 | ); // End Battery Screen
18 | }
19 |
20 | const styles = StyleSheet.create({
21 | container: {
22 | flex: 1,
23 | paddingTop: 15,
24 | backgroundColor: '#fff',
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/screens/ChargeScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ScrollView, StyleSheet } from 'react-native';
3 |
4 | import Level from '../components/Level';
5 |
6 | export default function ChargeScreen() {
7 | return (
8 |
9 |
16 |
17 | ); // End Charge Screen
18 | }
19 |
20 | const styles = StyleSheet.create({
21 | container: {
22 | flex: 1,
23 | paddingTop: 15,
24 | backgroundColor: '#fff',
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/store/AppContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createAppStore } from './AppState'
3 | import { useLocalObservable } from 'mobx-react'
4 |
5 | const AppContext = React.createContext(null);
6 |
7 | export const AppProvider = ({ children }) => {
8 | const appStore = useLocalObservable(createAppStore);
9 | appStore.loadSettings();
10 |
11 | return {children};
12 | };
13 |
14 | export const useAppStore = () => React.useContext(AppContext);
15 |
16 | /* HOC to inject store to any functional or class component */
17 | export const withAppStore = (Component) => (props) => {
18 | return ;
19 | };
20 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "Solar-Tracker",
4 | "description": "Display Solar Rest API Data",
5 | "slug": "solar",
6 | "privacy": "unlisted",
7 | "platforms": ["ios", "android"],
8 | "version": "1.0.40",
9 | "orientation": "portrait",
10 | "icon": "./assets/images/icon.png",
11 | "scheme": "myapp",
12 | "userInterfaceStyle": "automatic",
13 | "splash": {
14 | "image": "./assets/images/splash.png",
15 | "resizeMode": "contain",
16 | "backgroundColor": "#ffffff"
17 | },
18 | "updates": {
19 | "fallbackToCacheTimeout": 0
20 | },
21 | "assetBundlePatterns": [
22 | "**/*"
23 | ],
24 | "ios": {
25 | "supportsTablet": true
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/navigation/LinkingConfiguration.ts:
--------------------------------------------------------------------------------
1 | import * as Linking from 'expo-linking';
2 |
3 | export default {
4 | prefixes: [Linking.makeUrl('/')],
5 | config: {
6 | screens: {
7 | Root: {
8 | screens: {
9 | Panel: {
10 | screens: {
11 | PanelScreen: 'panel',
12 | },
13 | },
14 | Battery: {
15 | screens: {
16 | BatteryScreen: 'battery',
17 | },
18 | },
19 | Load: {
20 | screens: {
21 | LoadScreen: 'load',
22 | },
23 | },
24 | Charge: {
25 | screens: {
26 | ChargeScreen: 'charge',
27 | },
28 | },
29 | },
30 | },
31 | NotFound: '*',
32 | },
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StatusBar } from 'react-native';
3 | import { SafeAreaProvider } from 'react-native-safe-area-context';
4 |
5 | import useCachedResources from './hooks/useCachedResources';
6 | import useColorScheme from './hooks/useColorScheme';
7 | import Navigation from './navigation';
8 | import { AppProvider } from './store/AppContext';
9 |
10 | export default function App() {
11 | const isLoadingComplete = useCachedResources();
12 | const colorScheme = useColorScheme();
13 |
14 | if (!isLoadingComplete) {
15 | return null;
16 | } else {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/hooks/useCachedResources.ts:
--------------------------------------------------------------------------------
1 | import { Ionicons } from '@expo/vector-icons';
2 | import * as Font from 'expo-font';
3 | import * as SplashScreen from 'expo-splash-screen';
4 | import * as React from 'react';
5 |
6 | export default function useCachedResources() {
7 | const [isLoadingComplete, setLoadingComplete] = React.useState(false);
8 |
9 | // Load any resources or data that we need prior to rendering the app
10 | React.useEffect(() => {
11 | async function loadResourcesAndDataAsync() {
12 | try {
13 | SplashScreen.preventAutoHideAsync();
14 |
15 | // Load fonts
16 | await Font.loadAsync({
17 | ...Ionicons.font,
18 | 'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'),
19 | });
20 | } catch (e) {
21 | // We might want to provide this error information to an error reporting service
22 | console.warn(e);
23 | } finally {
24 | setLoadingComplete(true);
25 | SplashScreen.hideAsync();
26 | }
27 | }
28 |
29 | loadResourcesAndDataAsync();
30 | }, []);
31 |
32 | return isLoadingComplete;
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/python/solar_test.py:
--------------------------------------------------------------------------------
1 | from time import sleep
2 | from serial import Serial
3 |
4 | import sys
5 | sys.path.append('/home/pi/tracer/python')
6 | from tracer import Tracer, TracerSerial, QueryCommand
7 |
8 | port = Serial('/dev/ttyAMA0', 9600, timeout = 1)
9 | port.flushInput()
10 | port.flushOutput()
11 | tracer = Tracer(0x16)
12 | t_ser = TracerSerial(tracer, port)
13 | query = QueryCommand()
14 |
15 | try:
16 | while 1:
17 | try:
18 | t_ser.send_command(query)
19 | data = t_ser.receive_result()
20 | except (IndexError, IOError) as e:
21 | print(e)
22 | port.flushInput()
23 | port.flushOutput()
24 | sleep(4)
25 | continue
26 |
27 | print('Battery Voltage: {0:0.1f}V'.format(data.batt_voltage))
28 | print('Solar Panel Voltage: {0:0.1f}V'.format(data.pv_voltage))
29 | print('Charging Current: {0:0.2f}A'.format(data.charge_current))
30 | print('Load Current: {0:0.2f}A\n'.format(data.load_amps))
31 | sleep(4)
32 |
33 | except KeyboardInterrupt:
34 | print ("\nCtrl-C pressed. Closing serial port and exiting...")
35 | finally:
36 | port.close()
37 |
--------------------------------------------------------------------------------
/screens/NotFoundScreen.tsx:
--------------------------------------------------------------------------------
1 | import { StackScreenProps } from '@react-navigation/stack';
2 | import * as React from 'react';
3 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
4 |
5 | import { RootStackParamList } from '../types';
6 |
7 | export default function NotFoundScreen({
8 | navigation,
9 | }: StackScreenProps) {
10 | return (
11 |
12 | This screen doesn't exist.
13 | navigation.replace('Root')}
15 | style={styles.link}>
16 | Go to home screen!
17 |
18 |
19 | );
20 | }
21 |
22 | const styles = StyleSheet.create({
23 | container: {
24 | flex: 1,
25 | backgroundColor: '#fff',
26 | alignItems: 'center',
27 | justifyContent: 'center',
28 | padding: 20,
29 | },
30 | title: {
31 | fontSize: 20,
32 | fontWeight: 'bold',
33 | },
34 | link: {
35 | marginTop: 15,
36 | paddingVertical: 15,
37 | },
38 | linkText: {
39 | fontSize: 14,
40 | color: '#2e78b7',
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/python/solar-rest.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, jsonify
2 | from time import sleep
3 | from serial import Serial
4 |
5 | import sys
6 | sys.path.append('/home/pi/tracer/python')
7 | from tracer import Tracer, TracerSerial, QueryCommand
8 |
9 | port = Serial('/dev/ttyAMA0', 9600, timeout=1)
10 | port.flushInput()
11 | port.flushOutput()
12 | tracer = Tracer(0x16)
13 | t_ser = TracerSerial(tracer, port)
14 | query = QueryCommand()
15 |
16 |
17 | # Rest API
18 | app = Flask(__name__)
19 |
20 | @app.route('/solar', methods=['GET'])
21 | def get_data():
22 | try:
23 | t_ser.send_command(query)
24 | data = t_ser.receive_result()
25 |
26 | return jsonify(batt_voltage=data.batt_voltage,
27 | pv_voltage=data.pv_voltage,
28 | charge_current=data.charge_current,
29 | load_amps=data.load_amps)
30 |
31 | except (IndexError, IOError) as e:
32 | port.flushInput()
33 | port.flushOutput()
34 | return jsonify({'error': e.message}), 503
35 |
36 | try:
37 | app.run()
38 |
39 | except KeyboardInterrupt:
40 | print ("\nCtrl-C pressed. Closing serial port and exiting...")
41 | finally:
42 | port.close()
43 |
--------------------------------------------------------------------------------
/api/registerForPushNotificationsAsync.js:
--------------------------------------------------------------------------------
1 | import { Constants, Permissions, Notifications } from 'expo';
2 |
3 | // Example server, implemented in Rails: https://git.io/vKHKv
4 | const PUSH_ENDPOINT = 'https://expo-push-server.herokuapp.com/tokens';
5 |
6 | export default (async function registerForPushNotificationsAsync() {
7 | // Remote notifications do not work in simulators, only on device
8 | if (!Constants.isDevice) {
9 | return;
10 | }
11 |
12 | // Android remote notification permissions are granted during the app
13 | // install, so this will only ask on iOS
14 | let { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
15 |
16 | // Stop here if the user did not grant permissions
17 | if (status !== 'granted') {
18 | return;
19 | }
20 |
21 | // Get the token that uniquely identifies this device
22 | let token = await Notifications.getExpoPushTokenAsync();
23 |
24 | // POST the token to our backend so we can use it to send pushes from there
25 | return fetch(PUSH_ENDPOINT, {
26 | method: 'POST',
27 | headers: {
28 | Accept: 'application/json',
29 | 'Content-Type': 'application/json',
30 | },
31 | body: JSON.stringify({
32 | token: {
33 | value: token,
34 | },
35 | }),
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Solar-Tracker",
3 | "version": "1.0.40",
4 | "description": "Display Solar Rest API Data",
5 | "author": "rdagger",
6 | "license": "MIT",
7 | "main": "node_modules/expo/AppEntry.js",
8 | "private": true,
9 | "scripts": {
10 | "test": "node ./node_modules/jest/bin/jest.js --watchAll"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/rdagger/Expo-Solar-Tracker"
15 | },
16 | "jest": {
17 | "preset": "jest-expo"
18 | },
19 | "dependencies": {
20 | "@expo/samples": "3.0.3",
21 | "@expo/vector-icons": "^12.0.0",
22 | "@react-navigation/bottom-tabs": "5.11.2",
23 | "@react-navigation/native": "~5.8.10",
24 | "@react-navigation/stack": "~5.12.8",
25 | "expo": "~40.0.0",
26 | "expo-app-loading": "^1.0.1",
27 | "expo-asset": "~8.2.1",
28 | "expo-constants": "~9.3.0",
29 | "expo-font": "~8.4.0",
30 | "expo-linking": "~2.0.0",
31 | "expo-splash-screen": "~0.8.0",
32 | "mobx": "^6.1.7",
33 | "mobx-react": "^7.1.0",
34 | "react": "16.13.1",
35 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz",
36 | "react-native-gesture-handler": "~1.8.0",
37 | "react-native-safe-area-context": "3.1.9",
38 | "react-native-screens": "~2.15.0",
39 | "react-native-svg": "^12.1.0",
40 | "react-native-web": "~0.13.12",
41 | "@react-native-community/async-storage": "~1.12.0"
42 | },
43 | "devDependencies": {
44 | "babel-eslint": "^10.1.0",
45 | "eslint": "^7.20.0",
46 | "eslint-config-universe": "^7.0.1",
47 | "jest-expo": "~40.0.0",
48 | "prettier": "^2.2.1",
49 | "@types/react": "~16.9.35",
50 | "@types/react-native": "~0.63.2",
51 | "typescript": "~4.0.0"
52 | },
53 | "eslintConfig": {
54 | "extends": "universe/native"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/components/Level.js:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import PropTypes from 'prop-types';
3 | import React from 'react';
4 | import {
5 | ActivityIndicator,
6 | Dimensions,
7 | StyleSheet,
8 | Text,
9 | View,
10 | } from 'react-native';
11 |
12 | import { withAppStore } from '../store/AppContext';
13 | import Gauge from './Gauge';
14 |
15 | @withAppStore
16 | @observer
17 | class Level extends React.Component {
18 | render() {
19 | const { store } = this.props;
20 | const { height, width } = Dimensions.get('window');
21 | const gauge =
22 | store.data && typeof store.data[this.props.query] !== undefined ? (
23 |
36 | ) : (
37 |
38 | );
39 |
40 | return (
41 |
42 | {store.error}
43 | {gauge}
44 |
45 | );
46 | }
47 | } // End Level
48 | export default Level;
49 |
50 | Level.propTypes = {
51 | query: PropTypes.string.isRequired,
52 | label: PropTypes.string.isRequired,
53 | units: PropTypes.string,
54 | min: PropTypes.number,
55 | max: PropTypes.number,
56 | };
57 |
58 | const styles = StyleSheet.create({
59 | container: {
60 | flex: 1,
61 | },
62 | settingsButtonContainer: {
63 | alignItems: 'flex-end',
64 | marginRight: 25,
65 | },
66 | errorMessage: {
67 | fontSize: 20,
68 | color: 'red',
69 | textAlign: 'center',
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/components/SettingsButton.js:
--------------------------------------------------------------------------------
1 | import { FontAwesome } from '@expo/vector-icons';
2 | import { observer } from 'mobx-react';
3 | import React from 'react';
4 | import { Alert, TouchableOpacity, View } from 'react-native';
5 |
6 | import { withAppStore } from '../store/AppContext';
7 | import EditSettings from './EditSettings';
8 |
9 | @withAppStore
10 | @observer
11 | class SettingsButton extends React.Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this.state = {
16 | showEditSettings: false,
17 | disableForm: false,
18 | };
19 |
20 | this._onEditCancel = this._onEditCancel.bind(this);
21 | this._onEditSave = this._onEditSave.bind(this);
22 | this._onSettingsClick = this._onSettingsClick.bind(this);
23 | }
24 |
25 | render() {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
39 |
40 | );
41 | }
42 |
43 | _onEditCancel() {
44 | this.setState({ showEditSettings: false });
45 | }
46 |
47 | _onEditSave(settings) {
48 | this.setState({ disableForm: true });
49 | this.props.store.saveSettings(settings, (error) => {
50 | this.setState({ disableForm: false });
51 | if (error) {
52 | Alert.alert(error.message);
53 | } else {
54 | this.setState({ showEditSettings: false });
55 | }
56 | });
57 | }
58 |
59 | _onSettingsClick() {
60 | this.setState({ showEditSettings: true });
61 | }
62 | } // End SettingsButton
63 | export default SettingsButton;
64 |
--------------------------------------------------------------------------------
/navigation/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | NavigationContainer,
3 | DefaultTheme,
4 | DarkTheme,
5 | } from '@react-navigation/native';
6 | import { createStackNavigator } from '@react-navigation/stack';
7 | import * as React from 'react';
8 | import { ColorSchemeName, StyleSheet, View } from 'react-native';
9 |
10 | import { PiLogo } from '../components/PiLogo';
11 | import SettingsButton from '../components/SettingsButton';
12 | import NotFoundScreen from '../screens/NotFoundScreen';
13 | import { RootStackParamList } from '../types';
14 | import BottomTabNavigator from './BottomTabNavigator';
15 | import LinkingConfiguration from './LinkingConfiguration';
16 |
17 | // If you are not familiar with React Navigation, we recommend going through the
18 | // "Fundamentals" guide: https://reactnavigation.org/docs/getting-started
19 | export default function Navigation({
20 | colorScheme,
21 | }: {
22 | colorScheme: ColorSchemeName;
23 | }) {
24 | return (
25 |
28 |
29 |
30 | );
31 | }
32 |
33 | // A root stack navigator is often used for displaying modals on top of all other content
34 | // Read more here: https://reactnavigation.org/docs/modal
35 | const Stack = createStackNavigator();
36 |
37 |
38 | function RootNavigator() {
39 | return (
40 |
41 | (
47 |
48 |
49 |
50 | ),
51 | headerRight: () => (
52 |
53 |
54 |
55 | ),
56 | }}
57 | />
58 |
63 |
64 | ); // End Root Navigator
65 | }
66 |
67 | const styles = StyleSheet.create({
68 | settingsButtonContainer: {
69 | alignItems: 'flex-end',
70 | marginRight: 25,
71 | },
72 | logoContainer: {
73 | alignItems: 'flex-end',
74 | marginLeft: 25,
75 | },
76 | });
77 |
--------------------------------------------------------------------------------
/store/AppState.js:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-community/async-storage';
2 | import { runInAction } from 'mobx';
3 |
4 | export function createAppStore() {
5 | return {
6 | address: null,
7 | refresh: null,
8 | data: null,
9 | error: null,
10 | initialData: false,
11 |
12 | _updateData(data) {
13 | runInAction(() => {
14 | this.data = data;
15 | this.error = null;
16 | this.initialData = true;
17 | });
18 | },
19 |
20 | _fetchData() {
21 | if (!this.address) {
22 | runInAction(() => {
23 | this.error = 'No REST API address specified.\nPlease click settings.';
24 | });
25 | return;
26 | } else {
27 | runInAction(() => {
28 | this.error = null;
29 | });
30 | }
31 | // Fetch data from Rest API
32 | fetch(this.address, {
33 | method: 'GET',
34 | })
35 | .then((response) => {
36 | if (response.ok) {
37 | response.json().then((data) => {
38 | this._updateData(data);
39 | });
40 | } else {
41 | // Error
42 | runInAction(() => {
43 | if (response.status === 404) {
44 | this.error = '404 ERROR - The requested URL was NOT found.';
45 | } else {
46 | response.text().then((text) => (this.error = text));
47 | }
48 | });
49 | }
50 | // Repeat fetch at specified interval
51 | this.fetchTimer();
52 | })
53 | .catch((error) => {
54 | // Error no response
55 | runInAction(() => {
56 | this.error = error.message;
57 | });
58 |
59 | // Repeat fetch at specified interval
60 | this.fetchTimer();
61 | });
62 | },
63 | fetchTimer() {
64 | // Start timer if refresh interval specified
65 | if (this.refresh) {
66 | if (this.timeOut) {
67 | clearTimeout(this.timeOut);
68 | }
69 | // Note timer may not work properly if remote debugging enabled
70 | this.timeOut = setTimeout(
71 | () => this._fetchData(),
72 | parseFloat(this.refresh) * 1000
73 | );
74 | }
75 | },
76 |
77 | loadSettings() {
78 | AsyncStorage.multiGet(['address', 'refresh'], (error, stores) => {
79 | if (error) {
80 | console.log(error.message);
81 | }
82 | stores.map((result, i, store) => {
83 | // get at each store's key/value so you can work with it
84 |
85 | const key = store[i][0];
86 | const value = store[i][1];
87 | if (value) {
88 | runInAction(() => {
89 | this[key] = value;
90 | });
91 | }
92 | });
93 | }).then(() => {
94 | // Set default refresh interval of 60 seconds if not specified
95 | if (!this.refresh) {
96 | runInAction(() => {
97 | this.refresh = '15';
98 | });
99 | }
100 | this._fetchData();
101 | });
102 | },
103 |
104 | saveSettings(settings, onSave) {
105 | const { address, refresh } = settings;
106 | runInAction(() => {
107 | this.address = address;
108 | this.refresh = refresh;
109 | });
110 | const set_pairs = [
111 | ['address', address],
112 | ['refresh', refresh],
113 | ];
114 | AsyncStorage.multiSet(set_pairs, (error) => {
115 | if (error) {
116 | onSave(error.message);
117 | } else {
118 | onSave(null);
119 | runInAction(() => {
120 | this.error = null;
121 | });
122 | this._fetchData();
123 | }
124 | });
125 | },
126 | };
127 | }
128 |
--------------------------------------------------------------------------------
/components/Gauge.js:
--------------------------------------------------------------------------------
1 | // Code based on https://github.com/Reggino/react-svg-gauge
2 | import PropTypes from 'prop-types';
3 | import React from 'react';
4 | import { Svg, Text, Path } from 'react-native-svg';
5 |
6 | export default class Gauge extends React.Component {
7 | render() {
8 | const labelFontSize = this.props.labelFontSize
9 | ? this.props.labelFontSize
10 | : this.props.width / 10;
11 | const valueFontSize = this.props.valueFontSize
12 | ? this.props.valueFontSize
13 | : this.props.width / 5;
14 |
15 | const { Cx, Ro, Ri, Xo, Cy, Xi } = this._getPathValues(this.props.max);
16 | return (
17 |
57 | );
58 | }
59 |
60 | _getPathValues = (value) => {
61 | value = Math.min(value, this.props.max);
62 | value = Math.max(value, this.props.min);
63 |
64 | const a =
65 | (1 - (value - this.props.min) / (this.props.max - this.props.min)) *
66 | Math.PI;
67 | const SinA = Math.sin(a);
68 | const CosA = Math.cos(a);
69 |
70 | const Cx = this.props.width * 0.5;
71 | const Cy = this.props.height * 0.8;
72 |
73 | const Ro = Cx - this.props.width * 0.1;
74 | const Ri = Ro - this.props.width * 0.15;
75 |
76 | const Xo = Cx + Ro * CosA;
77 | const Yo = Cy - Ro * SinA;
78 | const Xi = Cx + Ri * CosA;
79 | const Yi = Cy - Ri * SinA;
80 |
81 | return { Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi };
82 | };
83 |
84 | _getPath = (value) => {
85 | const { Ro, Ri, Cx, Cy, Xo, Yo, Xi, Yi } = this._getPathValues(value);
86 |
87 | let path = 'M' + (Cx - Ri) + ',' + Cy + ' ';
88 | path += 'L' + (Cx - Ro) + ',' + Cy + ' ';
89 | path += 'A' + Ro + ',' + Ro + ' 0 0 1 ' + Xo + ',' + Yo + ' ';
90 | path += 'L' + Xi + ',' + Yi + ' ';
91 | path += 'A' + Ri + ',' + Ri + ' 0 0 0 ' + (Cx - Ri) + ',' + Cy + ' ';
92 |
93 | path += 'M' + (Cx - Ri * 0.9) + ',' + Cy + ' ';
94 | path +=
95 | 'A' +
96 | Ri * 0.9 +
97 | ',' +
98 | Ri * 0.9 +
99 | ' 0 0 0 ' +
100 | (Cx - Ri * 0.9) +
101 | ',' +
102 | Cy +
103 | ' ';
104 |
105 | path += 'Z ';
106 |
107 | return path;
108 | };
109 | } // End Gauge
110 |
111 | Gauge.propTypes = {
112 | value: PropTypes.number.isRequired,
113 | units: PropTypes.string,
114 | };
115 |
116 | Gauge.defaultProps = {
117 | label: 'React SVG Gauge',
118 | min: 0,
119 | max: 100,
120 | units: '',
121 | width: 400,
122 | height: 320,
123 | color: 'lime',
124 | backgroundColor: 'darkgreen',
125 | minMaxFontSize: 12,
126 | };
127 |
--------------------------------------------------------------------------------
/navigation/BottomTabNavigator.tsx:
--------------------------------------------------------------------------------
1 | import { Ionicons } from '@expo/vector-icons';
2 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
3 | import { createStackNavigator } from '@react-navigation/stack';
4 | import * as React from 'react';
5 | import { Platform } from 'react-native';
6 |
7 | import BatteryScreen from '../screens/BatteryScreen';
8 | import ChargeScreen from '../screens/ChargeScreen';
9 | import LoadScreen from '../screens/LoadScreen';
10 | import PanelScreen from '../screens/PanelScreen';
11 | import {
12 | BottomTabParamList,
13 | PanelParamList,
14 | BatteryParamList,
15 | LoadParamList,
16 | ChargeParamList,
17 | } from '../types';
18 |
19 | const BottomTab = createBottomTabNavigator();
20 |
21 | export default function BottomTabNavigator() {
22 | return (
23 |
24 | (
29 |
33 | ),
34 | }}
35 | />
36 | (
41 |
47 | ),
48 | }}
49 | />
50 | (
55 |
59 | ),
60 | }}
61 | />
62 | (
67 |
75 | ),
76 | }}
77 | />
78 |
79 | );
80 | }
81 |
82 | // You can explore the built-in icon families and icons on the web at:
83 | // https://icons.expo.fyi/
84 | function TabBarIcon(props: {
85 | name: React.ComponentProps['name'];
86 | color: string;
87 | }) {
88 | return ;
89 | }
90 |
91 | // Each tab has its own navigation stack, you can read more about this pattern here:
92 | // https://reactnavigation.org/docs/tab-based-navigation#a-stack-navigator-for-each-tab
93 | const PanelStack = createStackNavigator();
94 |
95 | function PanelNavigator() {
96 | return (
97 |
98 |
103 |
104 | );
105 | }
106 |
107 | const BatteryStack = createStackNavigator();
108 |
109 | function BatteryNavigator() {
110 | return (
111 |
112 |
117 |
118 | );
119 | }
120 |
121 | const LoadStack = createStackNavigator();
122 |
123 | function LoadNavigator() {
124 | return (
125 |
126 |
131 |
132 | );
133 | }
134 |
135 | const ChargeStack = createStackNavigator();
136 |
137 | function ChargeNavigator() {
138 | return (
139 |
140 |
145 |
146 | );
147 | }
148 |
--------------------------------------------------------------------------------
/python/xxv_tracer3.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Tracer Solar Regulator interface for MT-5 display
4 | #
5 | # Based on document by alexruhmann@body-soft.de
6 | # document version 3, 2011-12-13
7 | #
8 | # Verified on SainSonic MPPT Tracer 1215RN Solar Charge Controller
9 | # Regulator 12/24V INPUT 10A
10 | #
11 | # Modified by rdagger for Python3 (12-25-2019)
12 |
13 |
14 | class Result(object):
15 | """A command result from the controller."""
16 | props = []
17 |
18 | def __init__(self, data):
19 | self.data = data
20 | self.decode(data)
21 |
22 | def decode(self, data):
23 | """Decodes the result, storing data as fields"""
24 | pass
25 |
26 | def to_float(self, two_bytes):
27 | """Convert a list of two bytes into a floating point value."""
28 | # convert two bytes to a float value
29 | return ((two_bytes[1] << 8) | two_bytes[0]) / 100.0
30 |
31 | def __str__(self):
32 | return "%s{%s}" % (self.__class__.__name__, ", ".join(
33 | map(lambda a: "%s: %s" % (a, getattr(self, a)), self.props)))
34 |
35 |
36 | class QueryResult(Result):
37 | """The result of a query command."""
38 | props = ['batt_voltage', 'pv_voltage', 'load_amps',
39 | 'batt_overdischarge_voltage', 'batt_full_voltage', 'load_on',
40 | 'load_overload', 'load_short', 'batt_overload',
41 | 'batt_overdischarge', 'batt_full', 'batt_charging', 'batt_temp',
42 | 'charge_current']
43 |
44 | def decode(self, data):
45 | """Decodes the query result, storing results as fields"""
46 | if len(data) < 23:
47 | print('Not enough data. Need 23 bytes, got {0}'.format(len(data)))
48 | self.batt_voltage = self.to_float(data[0:2])
49 | self.pv_voltage = self.to_float(data[2:4])
50 | # [4:2] reserved; always 0
51 | self.load_amps = self.to_float(data[6:8])
52 | self.batt_overdischarge_voltage = self.to_float(data[8:10])
53 | self.batt_full_voltage = self.to_float(data[10:12])
54 | self.load_on = data[12] != 0
55 | self.load_overload = data[13] != 0
56 | self.load_short = data[14] != 0
57 | # data[15] reserved; always 0
58 | self.batt_overload = data[16] != 0
59 | self.batt_overdischarge = data[17] != 0
60 | self.batt_full = data[18] != 0
61 | self.batt_charging = data[19] != 0
62 | self.batt_temp = data[20] - 30
63 | self.charge_current = self.to_float(data[21:23])
64 |
65 |
66 | class Command(object):
67 | """A command sent to the controller"""
68 | def __init__(self, code, data=bytearray()):
69 | self.code = code
70 | self.data = data
71 |
72 | def decode_result(self, data):
73 | """Decodes the data, storing it in fields"""
74 | pass
75 |
76 |
77 | class QueryCommand(Command):
78 | """A command that queries the status of the controller"""
79 | def __init__(self):
80 | Command.__init__(self, 0xA0)
81 |
82 | def decode_result(self, data):
83 | return QueryResult(data)
84 |
85 |
86 | class ManualCommand(Command):
87 | """A command that turns the load on or off"""
88 | def __init__(self, state):
89 | if state:
90 | data = [0x01]
91 | else:
92 | data = [0x00]
93 | Command.__init__(self, 0xAA, data)
94 |
95 |
96 | class TracerSerial(object):
97 | """A serial interface to the Tracer"""
98 | sync_header = bytearray([0xEB, 0x90] * 3)
99 | comm_init = bytearray([0xAA, 0x55] * 3) + sync_header
100 |
101 | def __init__(self, tracer, port):
102 | """Create a new Tracer interface on the given serial port
103 | tracer is a Tracer() object
104 | port is an open serial port"""
105 | self.tracer = tracer
106 | self.port = port
107 |
108 | def to_bytes(self, command):
109 | """Converts the command into the bytes that should be sent"""
110 | cmd_data = self.tracer.get_command_bytes(command) + bytearray(b'\x00\x00\x7F')
111 | crc_data = self.tracer.add_crc(cmd_data)
112 | to_send = self.comm_init + crc_data
113 |
114 | return to_send
115 |
116 | def from_bytes(self, data):
117 | """Given bytes from serial port, returns appropriate command result"""
118 | if data[0:6] != self.sync_header:
119 | raise Exception("Invalid sync header")
120 | if len(data) != data[8] + 12:
121 | raise Exception("Invalid length. Expecting {0}, got {1}".format(
122 | data[8] + 12, len(data)))
123 | if not self.tracer.verify_crc(data[6:]):
124 | print("invalid crc")
125 | # raise Exception("Invalid CRC")
126 | return self.tracer.get_result(data[6:])
127 |
128 | def send_command(self, command):
129 | to_send = self.to_bytes(command)
130 | if len(to_send) != self.port.write(to_send):
131 | raise IOError("Error sending command: did not send all bytes")
132 |
133 | def receive_result(self):
134 | buff = bytearray()
135 | read_idx = 0
136 | to_read = 200
137 |
138 | while read_idx < (to_read + 12):
139 | b = bytearray(self.port.read(1))
140 | buff += b
141 | if read_idx < len(self.sync_header) and b[0] != self.sync_header[read_idx]:
142 | raise IOError("Error receiving result: invalid sync header")
143 | # the location of the read length
144 | elif read_idx == 8:
145 | to_read = b[0]
146 | read_idx += 1
147 | return self.from_bytes(buff)
148 |
149 |
150 | class Tracer(object):
151 | """An implementation of the Tracer MT-5 communication protocol"""
152 | def __init__(self, controller_id):
153 | """Create a new Tracer interface
154 | controller_id - the unit this was tested with is 0x16"""
155 | self.controller_id = controller_id
156 |
157 | def get_command_bytes(self, command):
158 | """Given a command, gets its byte representation
159 | This excludes the header, CRC, and trailer."""
160 | data = bytearray()
161 | data.append(self.controller_id)
162 | data.append(command.code)
163 | data.append(len(command.data))
164 | data += command.data
165 | return data
166 |
167 | def get_result(self, data):
168 | if data[1] == QueryCommand().code:
169 | return QueryResult(data[3:])
170 |
171 | def verify_crc(self, data):
172 | """Returns true if the CRC embedded in the data is valid"""
173 | verify = self.add_crc(data)
174 |
175 | return data == verify
176 |
177 | def add_crc(self, data):
178 | """Returns a copy of the data with the CRC added"""
179 | if len(data) < 6:
180 | raise Exception("data are too short")
181 | # the input CRC bytes must be zeroed
182 | data[data[2] + 3] = 0
183 | data[data[2] + 4] = 0
184 | crc = self.crc(data, data[2] + 5)
185 | data[data[2] + 3] = crc >> 8
186 | data[data[2] + 4] = crc & 0xFF
187 |
188 | return data
189 |
190 | def crc(self, data, crc_len):
191 | """Calculates the Tracer CRC for the given data"""
192 | i = j = r1 = r2 = r3 = r4 = 0
193 | result = 0
194 |
195 | r1 = data[0]
196 | r2 = data[1]
197 | crc_buff = 2
198 |
199 | for i in range(0, crc_len - 2):
200 | r3 = data[crc_buff]
201 | crc_buff += 1
202 |
203 | for j in range(0, 8):
204 | r4 = r1
205 | r1 = (r1 * 2) & 0xFF
206 |
207 | if r2 & 0x80:
208 | r1 += 1
209 | r2 = (r2 * 2) & 0xFF
210 |
211 | if r3 & 0x80:
212 | r2 += 1
213 | r3 = (r3 * 2) & 0xFF
214 |
215 | if r4 & 0x80:
216 | r1 ^= 0x10
217 | r2 ^= 0x41
218 |
219 | result = (r1 << 8) | r2
220 | return result
221 |
--------------------------------------------------------------------------------
/components/EditSettings.js:
--------------------------------------------------------------------------------
1 | import { FontAwesome } from '@expo/vector-icons';
2 | import { observer } from 'mobx-react';
3 | import PropTypes from 'prop-types';
4 | import React from 'react';
5 | import {
6 | Alert,
7 | Modal,
8 | StyleSheet,
9 | Text,
10 | TextInput,
11 | TouchableOpacity,
12 | View,
13 | } from 'react-native';
14 |
15 | import { withAppStore } from '../store/AppContext';
16 |
17 | @withAppStore
18 | @observer
19 | class EditSettings extends React.Component {
20 | constructor(props) {
21 | super(props);
22 |
23 | const { store } = this.props;
24 |
25 | this.state = {
26 | address: store.address,
27 | refresh: store.refresh,
28 | addressError: false,
29 | refreshError: false,
30 | };
31 |
32 | this._onCancel = this._onCancel.bind(this);
33 | this._onSave = this._onSave.bind(this);
34 | this._onChangeAddress = this._onChangeAddress.bind(this);
35 | this._onChangeRefresh = this._onChangeRefresh.bind(this);
36 | this._onEndAddress = this._onEndAddress.bind(this);
37 | this._onEndRefresh = this._onEndRefresh.bind(this);
38 | this._onShow = this._onShow.bind(this);
39 | }
40 |
41 | render() {
42 | const errorStyle = {
43 | borderColor: 'red',
44 | backgroundColor: 'mistyrose',
45 | };
46 | const errorIcon = (
47 |
48 | );
49 |
50 | const addressErrorStyle = this.state.addressError ? errorStyle : null;
51 | const addressErrorIcon = this.state.addressError ? errorIcon : null;
52 | const refreshErrorStyle = this.state.refreshError ? errorStyle : null;
53 | const refreshErrorIcon = this.state.refreshError ? errorIcon : null;
54 | return (
55 |
56 |
62 |
63 |
64 | Address {addressErrorIcon}
65 |
78 |
79 |
80 | Refresh Interval {refreshErrorIcon}
81 |
82 |
95 |
96 |
97 |
98 |
103 |
104 | Cancel
105 |
106 |
107 |
112 |
113 | Save
114 |
115 |
116 |
117 |
118 |
119 |
120 | );
121 | }
122 |
123 | _onCancel() {
124 | this.props.onCancelEdit();
125 | }
126 |
127 | _closeModal() {
128 | console.log('closemodal');
129 | }
130 |
131 | _onSave() {
132 | const address = this.state.address;
133 | const refresh = this.state.refresh;
134 |
135 | if (this.state.addressError) {
136 | Alert.alert('Please fix address.');
137 | } else if (this.state.refreshError) {
138 | Alert.alert('Please fix refresh interval.');
139 | } else if (!address) {
140 | Alert.alert('Please enter address.');
141 | } else if (!refresh) {
142 | Alert.alert('Please enter refresh interval.');
143 | } else {
144 | this.props.onSaveEdit({
145 | address,
146 | refresh,
147 | });
148 | }
149 | }
150 |
151 | _onShow() {
152 | this.setState({ address: this.props.store.address });
153 | this.setState({ refresh: this.props.store.refresh });
154 | this.setState({ addressError: false });
155 | this.setState({ refreshError: false });
156 | }
157 |
158 | _onChangeAddress(address) {
159 | this.setState({ address });
160 | this.setState({ addressError: false });
161 | }
162 |
163 | _onChangeRefresh(refresh) {
164 | this.setState({ refresh });
165 | this.setState({ refreshError: false });
166 | }
167 |
168 | _onEndAddress() {
169 | const address = this.state.address;
170 | if (
171 | address &&
172 | address.length > 4 &&
173 | address.toUpperCase().startsWith('HTTP')
174 | ) {
175 | // Store address
176 | this.setState({ addressError: false });
177 | } else {
178 | // Invalid address
179 | this.setState({ addressError: true });
180 | }
181 | }
182 |
183 | _onEndRefresh() {
184 | const refresh = this.state.refresh;
185 | if (refresh && !isNaN(parseFloat(refresh)) && isFinite(refresh)) {
186 | // Store refresh interval
187 | this.setState({ refreshError: false });
188 | } else {
189 | // Invalid refresh
190 | this.setState({ refreshError: true });
191 | }
192 | }
193 | } // End EditSettings
194 | export default EditSettings;
195 |
196 | EditSettings.propTypes = {
197 | visible: PropTypes.bool.isRequired,
198 | onCancelEdit: PropTypes.func.isRequired,
199 | onSaveEdit: PropTypes.func.isRequired,
200 | };
201 |
202 | const styles = StyleSheet.create({
203 | innerContainer: {
204 | flex: 1,
205 | paddingVertical: 50,
206 | paddingHorizontal: 20,
207 | justifyContent: 'flex-start',
208 | backgroundColor: 'rgba(10, 0, 50, 0.8)',
209 | },
210 | inputContainer: {
211 | borderTopLeftRadius: 20,
212 | borderTopRightRadius: 20,
213 | backgroundColor: 'white',
214 | paddingVertical: 40,
215 | },
216 | inputLabel: {
217 | marginLeft: 35,
218 | fontSize: 20,
219 | alignItems: 'flex-start',
220 | },
221 | buttonsContainer: {
222 | borderBottomLeftRadius: 20,
223 | borderBottomRightRadius: 20,
224 | backgroundColor: 'lightgrey',
225 | flexDirection: 'row',
226 | justifyContent: 'space-around',
227 | padding: 10,
228 | },
229 | button: {
230 | borderColor: 'black',
231 | alignItems: 'center',
232 | justifyContent: 'center',
233 | height: 48,
234 | paddingHorizontal: 18,
235 | borderRadius: 48 / 2,
236 | borderWidth: 1,
237 | },
238 | buttonSave: {
239 | backgroundColor: 'chartreuse',
240 | paddingHorizontal: 10,
241 | },
242 | buttonCancel: {
243 | backgroundColor: '#DE4567',
244 | },
245 | buttonLabel: {
246 | fontSize: 24,
247 | },
248 | textInput: {
249 | alignItems: 'stretch',
250 | borderColor: 'cornflowerblue',
251 | backgroundColor: 'aliceblue',
252 | borderRadius: 10,
253 | borderWidth: 1,
254 | fontSize: 20,
255 | height: 56,
256 | padding: 10,
257 | marginHorizontal: 30,
258 | marginBottom: 20,
259 | },
260 | });
261 |
--------------------------------------------------------------------------------