├── .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 | [![Raspberry Pi Solar Serial REST API Tutorial](http://img.youtube.com/vi/1VxP38XlVEQ/0.jpg)](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 | 18 | 22 | 23 | 30 | {this.props.label} 31 | 32 | 38 | {this.props.value.toString() + this.props.units} 39 | 40 | 46 | {this.props.min} 47 | 48 | 54 | {this.props.max} 55 | 56 | 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 | --------------------------------------------------------------------------------