├── public ├── favicon.ico ├── apple-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── ms-icon-70x70.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── android-icon-144x144.png ├── android-icon-192x192.png ├── apple-icon-precomposed.png ├── img │ ├── 5eab7acf04f85fad0b3ab6cd7ea86a29.png │ ├── 34025473da5c323aad4c8e8af49c1b66.svg │ ├── 606298438bdfd9a7d4ed4872d38016c2.svg │ ├── 383c7246f449467e582cb88956ee7205.svg │ ├── 8524b5f7d788cb6342232f2fb5b3cc35.svg │ ├── a01ba472697014262575a97198f95bf8.svg │ ├── c57e7b91e757beafc7f7541669faaf41.svg │ ├── 441c3faab80588ed741d9e7699528264.svg │ ├── 4a4d24bbfb74705aecbdcc3089fbc98d.svg │ ├── 90f3a8e4d6ad7e7ea0686704b359a1a1.svg │ ├── e81675393f46961ef45e8569f706e57f.svg │ ├── 7003f03d18f19ca4727bcdf812992708.svg │ ├── eb8f2e3543626f39af88bbb6b824633c.svg │ ├── f9bae092fc44af2d2317004056f6f61b.svg │ ├── 7d355afd2429e133780ec451d31811a9.svg │ ├── d7fa658fb88526d4bbbaddd0ed9ce6eb.svg │ ├── e1d081aa4b0cb52dd3c2544f65f47bfe.svg │ ├── e5e0a3c2a878c19b91d70a1f1b27b7f5.svg │ └── 4dcef6dff88afbb4d1197b1b9a9eb05d.svg ├── fonts │ ├── 0942d1e1c447d6ce3ffcc75168766387.ttf │ ├── 371511ddba5fdac0538c83f41d8fa022.woff │ ├── bb2ec001c20cf752f8520e72eb49cce6.eot │ ├── c422543605bc94796ef2c68787f08d05.woff │ ├── d816f52b2403c9a1cd9bc5591c8f57d4.eot │ ├── dbf02e411277baca75336cfa3395bc3a.ttf │ └── e218aee6928fe3da9ed6e0a1d3adc8be.woff2 ├── browserconfig.xml ├── manifest.4aa7768517a92254474bf67090b301ec.json └── bundle.437619a9224327bb3054.js.LICENSE.txt ├── rpi ├── background.png ├── weston.sh ├── .bash_profile ├── weston.ini ├── cbatmo.service └── deploy.sh ├── deploy ├── systemd │ ├── override.conf │ └── cbatmo.service ├── weston │ ├── background.png │ ├── weston.sh │ └── weston.ini ├── .bash_profile └── webkit │ └── browser ├── screenshots ├── real_001.jpg ├── real_002.jpg ├── real_003.jpg ├── real_004.jpg ├── cbatmo_deploy.gif ├── screenshot_001.png ├── screenshot_002.png ├── screenshot_003.png ├── screenshot_004.png ├── screenshot_005.png ├── screenshot_006.png ├── screenshot_007.png ├── screenshot_008.png ├── screenshot_009.png ├── screenshot_010.png ├── screenshot_011.png ├── cbatmo_mobile_001.png ├── cbatmo_mobile_002.png └── cbatmo_mobile_003.png ├── src ├── img │ ├── world_map_800.png │ ├── world_map_point.png │ ├── world_map_800x480.png │ ├── world_map_point_color.png │ ├── battery_full.svg │ ├── battery_max.svg │ ├── battery_low.svg │ ├── battery_high.svg │ ├── signal_1.svg │ ├── signal_2.svg │ ├── signal_3.svg │ ├── signal_4.svg │ ├── signal_5.svg │ ├── battery_medium.svg │ ├── battery_very-low.svg │ ├── netatmo_station.svg │ ├── netatmo_indoor_module.svg │ ├── netatmo_outdoor_module.svg │ ├── wifi_1.svg │ ├── wifi_2.svg │ ├── wifi_3.svg │ ├── wifi_4.svg │ ├── netatmo_wind_module.svg │ └── netatmo_rain_module.svg ├── css │ ├── digital-7 (mono).eot │ ├── digital-7 (mono).ttf │ ├── digital-7 (mono).woff │ ├── _variables.scss │ └── style.scss ├── types │ ├── global.d.ts │ ├── custom.d.ts │ └── netatmo.ts ├── store │ ├── openweather │ │ ├── types.ts │ │ ├── reducer.ts │ │ └── actions.ts │ ├── application │ │ ├── actions.ts │ │ ├── types.ts │ │ └── reducer.ts │ ├── index.ts │ └── netatmo │ │ └── types.ts ├── containers │ ├── ModuleForecastContainer.ts │ ├── ModuleNetatmoRainGraphContainer.ts │ ├── ModuleDateTimeContainer.ts │ ├── AppContainer.ts │ ├── ModuleNetatmoInformationContainer.ts │ ├── ModuleNetatmoWindContainer.ts │ ├── ModuleNetatmoRainContainer.ts │ ├── ModuleNetatmoGraphContainer.ts │ ├── ModuleNetatmoBarometerContainer.ts │ ├── ModuleNetatmoStationContainer.ts │ ├── AppStartingContainer.ts │ ├── ModuleNetatmoOutdoorContainer.ts │ ├── ModuleNetatmoIndoorContainer.ts │ ├── ModuleNetatmoIndoorThirdContainer.ts │ └── ModuleNetatmoIndoorSecondContainer.ts ├── configureStore.ts ├── models │ ├── WeatherInterface.ts │ ├── NetatmoChartsData.ts │ ├── DarkskyData.ts │ ├── OpenWeatherData.ts │ ├── NetatmoNAModule3.ts │ ├── NetatmoNAModule1.ts │ ├── NetatmoNAModule4.ts │ ├── NetatmoNAModule2.ts │ └── NetatmoUserInformation.ts ├── components │ ├── ErrorBoundary.tsx │ ├── ModuleNetatmoNotReachable.tsx │ ├── ModuleNetatmoRainGraph.tsx │ ├── ModuleForecastDaily.tsx │ ├── ModuleNetatmoBarometer.tsx │ ├── ModuleNetatmoInformation.tsx │ ├── WeatherIcon.tsx │ ├── ModuleForecast.tsx │ ├── ModuleNetatmoRain.tsx │ ├── ModuleNetatmoWind.tsx │ ├── ModuleDateTime.tsx │ ├── ModuleNetatmoOutdoor.tsx │ ├── ModuleNetatmoGraph.tsx │ ├── ModuleNetatmoStation.tsx │ └── ModuleNetatmoIndoor.tsx ├── i18n │ ├── fr │ │ └── common.json │ ├── en │ │ └── common.json │ └── de │ │ └── common.json ├── utils │ └── tools.ts ├── layouts │ ├── MainLayout.tsx │ ├── DashboardLayoutContainer.ts │ └── DashboardLayout.tsx ├── index.tsx └── index.ejs ├── views ├── error.ejs └── index.ejs ├── .env.dist ├── .gitignore ├── .github └── FUNDING.yml ├── tsconfig.json ├── LICENSE ├── CHANGELOG.md ├── package.json └── webpack.config.js /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /rpi/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/rpi/background.png -------------------------------------------------------------------------------- /deploy/systemd/override.conf: -------------------------------------------------------------------------------- 1 | [Service] 2 | ExecStart= 3 | ExecStart=-/sbin/agetty -nia pi %I -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /screenshots/real_001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/real_001.jpg -------------------------------------------------------------------------------- /screenshots/real_002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/real_002.jpg -------------------------------------------------------------------------------- /screenshots/real_003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/real_003.jpg -------------------------------------------------------------------------------- /screenshots/real_004.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/real_004.jpg -------------------------------------------------------------------------------- /src/img/world_map_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/src/img/world_map_800.png -------------------------------------------------------------------------------- /deploy/weston/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/deploy/weston/background.png -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /src/css/digital-7 (mono).eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/src/css/digital-7 (mono).eot -------------------------------------------------------------------------------- /src/css/digital-7 (mono).ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/src/css/digital-7 (mono).ttf -------------------------------------------------------------------------------- /src/img/world_map_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/src/img/world_map_point.png -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /screenshots/cbatmo_deploy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/cbatmo_deploy.gif -------------------------------------------------------------------------------- /screenshots/screenshot_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_001.png -------------------------------------------------------------------------------- /screenshots/screenshot_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_002.png -------------------------------------------------------------------------------- /screenshots/screenshot_003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_003.png -------------------------------------------------------------------------------- /screenshots/screenshot_004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_004.png -------------------------------------------------------------------------------- /screenshots/screenshot_005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_005.png -------------------------------------------------------------------------------- /screenshots/screenshot_006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_006.png -------------------------------------------------------------------------------- /screenshots/screenshot_007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_007.png -------------------------------------------------------------------------------- /screenshots/screenshot_008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_008.png -------------------------------------------------------------------------------- /screenshots/screenshot_009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_009.png -------------------------------------------------------------------------------- /screenshots/screenshot_010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_010.png -------------------------------------------------------------------------------- /screenshots/screenshot_011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/screenshot_011.png -------------------------------------------------------------------------------- /src/css/digital-7 (mono).woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/src/css/digital-7 (mono).woff -------------------------------------------------------------------------------- /src/img/world_map_800x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/src/img/world_map_800x480.png -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/android-icon-192x192.png -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /screenshots/cbatmo_mobile_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/cbatmo_mobile_001.png -------------------------------------------------------------------------------- /screenshots/cbatmo_mobile_002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/cbatmo_mobile_002.png -------------------------------------------------------------------------------- /screenshots/cbatmo_mobile_003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/screenshots/cbatmo_mobile_003.png -------------------------------------------------------------------------------- /src/img/world_map_point_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/src/img/world_map_point_color.png -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /** Extend window variable with preloadedState */ 2 | interface Window { 3 | preloadedState: any 4 | } 5 | -------------------------------------------------------------------------------- /public/img/5eab7acf04f85fad0b3ab6cd7ea86a29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/img/5eab7acf04f85fad0b3ab6cd7ea86a29.png -------------------------------------------------------------------------------- /public/fonts/0942d1e1c447d6ce3ffcc75168766387.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/fonts/0942d1e1c447d6ce3ffcc75168766387.ttf -------------------------------------------------------------------------------- /public/fonts/371511ddba5fdac0538c83f41d8fa022.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/fonts/371511ddba5fdac0538c83f41d8fa022.woff -------------------------------------------------------------------------------- /public/fonts/bb2ec001c20cf752f8520e72eb49cce6.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/fonts/bb2ec001c20cf752f8520e72eb49cce6.eot -------------------------------------------------------------------------------- /public/fonts/c422543605bc94796ef2c68787f08d05.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/fonts/c422543605bc94796ef2c68787f08d05.woff -------------------------------------------------------------------------------- /public/fonts/d816f52b2403c9a1cd9bc5591c8f57d4.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/fonts/d816f52b2403c9a1cd9bc5591c8f57d4.eot -------------------------------------------------------------------------------- /public/fonts/dbf02e411277baca75336cfa3395bc3a.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/fonts/dbf02e411277baca75336cfa3395bc3a.ttf -------------------------------------------------------------------------------- /deploy/.bash_profile: -------------------------------------------------------------------------------- 1 | [[ -f ~/.bashrc ]] && . ~/.bashrc 2 | if [[ ! $DISPLAY && $XDG_VTNR -eq 1 ]]; then 3 | exec ~/cbatmo/deploy/weston/weston.sh 4 | fi 5 | -------------------------------------------------------------------------------- /public/fonts/e218aee6928fe3da9ed6e0a1d3adc8be.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gulivertx/cbatmo/HEAD/public/fonts/e218aee6928fe3da9ed6e0a1d3adc8be.woff2 -------------------------------------------------------------------------------- /rpi/weston.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | weston & 3 | sleep 4s 4 | export WAYLAND_DISPLAY=wayland-0 5 | export DISPLAY=:1 6 | exec ~/.browse --fullscreen http://localhost:3000 -------------------------------------------------------------------------------- /src/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: React.FunctionComponent>; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /rpi/.bash_profile: -------------------------------------------------------------------------------- 1 | # 2 | # ~/.bash_profile 3 | # 4 | 5 | [[ -f ~/.bashrc ]] && . ~/.bashrc 6 | if [[ ! $DISPLAY && $XDG_VTNR -eq 1 ]]; then 7 | exec ~/cbatmo/rpi/weston.sh 8 | fi -------------------------------------------------------------------------------- /src/css/_variables.scss: -------------------------------------------------------------------------------- 1 | /** COLORS **/ 2 | $dark: #1E1E1E; 3 | $white: #f5f8fa; 4 | $gray: #909090; 5 | $blue: #106BA3; 6 | $red: #C23030; 7 | $yellow: #C07326; 8 | $green: #0D8051; 9 | -------------------------------------------------------------------------------- /deploy/weston/weston.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | weston & 3 | sleep 4s 4 | export WAYLAND_DISPLAY=wayland-0 5 | export DISPLAY=:1 6 | exec ~/cbatmo/deploy/webkit/.browser --fullscreen http://localhost:3000 7 | -------------------------------------------------------------------------------- /public/img/34025473da5c323aad4c8e8af49c1b66.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/606298438bdfd9a7d4ed4872d38016c2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rpi/weston.ini: -------------------------------------------------------------------------------- 1 | [core] 2 | backend=drm-backend.so 3 | xwayland=false 4 | idle-time=0 5 | 6 | [keyboard] 7 | keymap_layout=ch(fr) 8 | 9 | [shell] 10 | panel-location="" 11 | panel-position=none 12 | background-color=0x00000000 13 | background-image=/home/alarm/cbatmo/rpi/background.png -------------------------------------------------------------------------------- /public/img/383c7246f449467e582cb88956ee7205.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/8524b5f7d788cb6342232f2fb5b3cc35.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/a01ba472697014262575a97198f95bf8.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /public/manifest.4aa7768517a92254474bf67090b301ec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CBatmo", 3 | "short_name": "CBatmo", 4 | "orientation": "landscape", 5 | "display": "standalone", 6 | "start_url": ".", 7 | "description": "A Netatmo Weather Station Web-APP for Raspberry Pi and official Raspberry touchscreen", 8 | "background_color": "#1E1E1E" 9 | } -------------------------------------------------------------------------------- /public/img/c57e7b91e757beafc7f7541669faaf41.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/441c3faab80588ed741d9e7699528264.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/4a4d24bbfb74705aecbdcc3089fbc98d.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/90f3a8e4d6ad7e7ea0686704b359a1a1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/e81675393f46961ef45e8569f706e57f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= message %> 9 | 10 |

<%= message %>

11 |

<%= error.status %>

12 |
<%= error.stack %>
13 | 14 | 15 | -------------------------------------------------------------------------------- /public/img/7003f03d18f19ca4727bcdf812992708.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/eb8f2e3543626f39af88bbb6b824633c.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/f9bae092fc44af2d2317004056f6f61b.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/7d355afd2429e133780ec451d31811a9.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/d7fa658fb88526d4bbbaddd0ed9ce6eb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/e1d081aa4b0cb52dd3c2544f65f47bfe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | # Can be dev or prod 2 | # If you want to deploy CBatmo to your Pi and just using it please use prod 3 | # If you want to contribute or make any change of CBatmo use dev 4 | APP_ENV=dev 5 | 6 | # Secret key to login to cbatmo 7 | APP_SECRET= 8 | 9 | # OpenWeather key 10 | # Get an OpenWeather API key from dev area : https://openweathermap.org/ 11 | OPENWEATHER_API_KEY= 12 | 13 | # Netatmo 14 | # Get a Netatmo credential from dev area : https://dev.netatmo.com/ 15 | NETATMO_CLIENT_ID= 16 | NETATMO_CLIENT_SECRET= 17 | -------------------------------------------------------------------------------- /deploy/weston/weston.ini: -------------------------------------------------------------------------------- 1 | [core] 2 | shell=desktop-shell.so 3 | backend=fbdev-backend.so 4 | xwayland=false 5 | idle-time=0 6 | require-input=false 7 | 8 | [keyboard] 9 | keymap_layout=ch(fr) 10 | 11 | [libinput] 12 | enable-tap=true 13 | 14 | [shell] 15 | background-type=scale-crop 16 | background-color=0x00000000 17 | background-image=/home/pi/cbatmo/deploy/weston/background.png 18 | 19 | [launcher] 20 | icon=/home/pi/cbatmo/public/android-icon-36x36.png 21 | path=/home/pi/cbatmo/deploy/webkit/browser --fullscreen http://localhost:3000 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Dependency directories 19 | node_modules/ 20 | 21 | # Optional npm cache directory 22 | .npm 23 | 24 | # Yarn Integrity file 25 | .yarn-integrity 26 | 27 | # dotenv environment variables file 28 | .env 29 | 30 | # System Files 31 | .DS_Store 32 | Thumbs.db 33 | !/deploy 34 | /deploy/infomaniak_deploy.sh 35 | /deploy/infomaniak_install.sh 36 | -------------------------------------------------------------------------------- /public/img/e5e0a3c2a878c19b91d70a1f1b27b7f5.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/openweather/types.ts: -------------------------------------------------------------------------------- 1 | import {IOpenWeatherData} from "../../models/OpenWeatherData"; 2 | 3 | export enum OpenWeatherActionTypes { 4 | REQUEST = '@@openweather/REQUEST', 5 | SUCCESS = '@@openweather/SUCCESS', 6 | FAILURE = '@@openweather/FAILURE' 7 | } 8 | 9 | // The complete state for the store 10 | export interface IOpenWeatherState { 11 | readonly loading: boolean 12 | readonly first_fetch: boolean 13 | readonly data: IOpenWeatherData|undefined 14 | readonly updated_at: number 15 | readonly errors: any|undefined 16 | } 17 | -------------------------------------------------------------------------------- /deploy/systemd/cbatmo.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=CBatmo WEB server 3 | # Set dependencies to other services (optional) 4 | #After=mongodb.service 5 | 6 | [Service] 7 | # Start the js-file starting the express server 8 | ExecStart=/usr/bin/yarn start 9 | WorkingDirectory=/home/pi/cbatmo 10 | Restart=always 11 | RestartSec=10 12 | StandardOutput=syslog 13 | StandardError=syslog 14 | SyslogIdentifier=CBatmo 15 | # Change to a non-root user (optional, but recommended) 16 | User=pi 17 | Group=pi 18 | # Set environment options 19 | Environment=NODE_ENV=production PORT=3000 20 | 21 | [Install] 22 | WantedBy=multi-user.target 23 | -------------------------------------------------------------------------------- /src/types/netatmo.ts: -------------------------------------------------------------------------------- 1 | export enum MODULE_TYPE { 2 | MAIN = 'NAMain', 3 | INDOOR = 'NAModule4', 4 | INDOOR_SECOND = 'NAModule4', 5 | INDOOR_THIRD = 'NAModule4', 6 | OUTDOOR = 'NAModule1', 7 | RAIN = 'NAModule3', 8 | WIND = 'NAModule2' 9 | } 10 | 11 | export type WifiLevel = '1'|'2'|'3'|'4'; 12 | export type RadioLevel = '1'|'2'|'3'|'4'|'5'; 13 | export type BatteryLevel = 'very-low'|'low'|'medium'|'high'|'full'|'max'; 14 | 15 | export type ChartScales = '30min'|'1hour'|'3hours'|'1day'|'1week'|'1month'; 16 | 17 | export type Timelapse = '12h'|'1d'|'1m'; 18 | 19 | export type DataTypes = 'Temperature'|'CO2'|'Humidity'|'Noise'|'Pressure'; 20 | -------------------------------------------------------------------------------- /src/img/battery_full.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/battery_max.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/battery_low.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/battery_high.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/signal_1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/signal_2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/signal_3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/signal_4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/signal_5.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Gulivertx] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/img/battery_medium.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/battery_very-low.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/containers/ModuleForecastContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleForecast from "../components/ModuleForecast" 5 | 6 | const mapStateToProps = ({ openweather, application}: ApplicationState) => ({ 7 | openweather: openweather, 8 | locale: application.user.lang, 9 | phone: application.phone, 10 | orientation: application.orientation 11 | }); 12 | 13 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 14 | 15 | }); 16 | 17 | const ModuleForecastContainer = connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(ModuleForecast); 21 | 22 | export default ModuleForecastContainer 23 | -------------------------------------------------------------------------------- /src/img/netatmo_station.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/store/application/actions.ts: -------------------------------------------------------------------------------- 1 | import {ApplicationActionTypes, Orientation} from './types' 2 | import {INetatmoUserInformation} from "../../models/NetatmoUserInformation"; 3 | 4 | /** Application actions **/ 5 | export const appConfigured = (value: boolean) => { 6 | return { 7 | type: ApplicationActionTypes.APP_CONFIGURED, 8 | payload: value 9 | } 10 | }; 11 | 12 | export const setUserInfo = (user: INetatmoUserInformation) => { 13 | return { 14 | type: ApplicationActionTypes.USER_INFO, 15 | payload: user 16 | } 17 | }; 18 | 19 | export const setOrientation = (orientation: Orientation) => { 20 | return { 21 | type: ApplicationActionTypes.DEVICE_ORIENTATION, 22 | payload: orientation 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { Store, createStore, applyMiddleware, compose } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | 5 | /** Import the state interface and combined reducers */ 6 | import { ApplicationState, rootReducer } from './store' 7 | 8 | export default function configureStore( 9 | initialState: ApplicationState 10 | ): Store { 11 | // Create the composing function for our middlewares 12 | const composeEnhancers = composeWithDevTools({}) || compose; 13 | 14 | return createStore( 15 | rootReducer, 16 | initialState, 17 | composeEnhancers( 18 | applyMiddleware(thunkMiddleware) 19 | ) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoRainGraphContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch } from "redux-thunk"; 3 | import ModuleNetatmoRainGraph from "../components/ModuleNetatmoRainGraph"; 4 | import {ApplicationState} from "../store"; 5 | 6 | const mapStateToProps = ({ netatmo, application }: ApplicationState) => ({ 7 | data: netatmo.measure_rain_data, 8 | phone: application.phone, 9 | mobile: application.mobile, 10 | orientation: application.orientation, 11 | }); 12 | 13 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 14 | 15 | }); 16 | 17 | const ModuleNetatmoRainGraphContainer = connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(ModuleNetatmoRainGraph); 21 | 22 | export default ModuleNetatmoRainGraphContainer 23 | -------------------------------------------------------------------------------- /src/containers/ModuleDateTimeContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleDateTime from "../components/ModuleDateTime" 5 | 6 | const mapStateToProps = ({ openweather, application}: ApplicationState) => ({ 7 | sunset_time: openweather.data?.daily.data[0].sunset_time, 8 | sunrise_time: openweather.data?.daily.data[0].sunrise_time, 9 | locale: application.user.lang, 10 | orientation: application.orientation, 11 | }); 12 | 13 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 14 | 15 | }); 16 | 17 | const ModuleDateTimeContainer = connect( 18 | mapStateToProps, 19 | mapDispatchToProps 20 | )(ModuleDateTime); 21 | 22 | export default ModuleDateTimeContainer 23 | -------------------------------------------------------------------------------- /src/models/WeatherInterface.ts: -------------------------------------------------------------------------------- 1 | export interface IPlace { 2 | timezone: string 3 | latitude: number 4 | longitude: number 5 | } 6 | 7 | export interface ICurrently { 8 | time: number 9 | temperature: number 10 | humidity: number 11 | pressure: number 12 | wind_speed: number 13 | wind_gust: number 14 | uv_index: number 15 | ozone: number 16 | icon: string 17 | summary: string 18 | } 19 | 20 | export interface IDaily { 21 | summary: string 22 | icon: string 23 | data: IDailyData[] 24 | } 25 | 26 | export interface IDailyData { 27 | time: number 28 | summary: string 29 | icon: string 30 | icon_id?: number 31 | sunrise_time: number 32 | sunset_time: number 33 | moon_phase: number 34 | temperature_low: number 35 | temperature_high: number 36 | } 37 | -------------------------------------------------------------------------------- /src/img/netatmo_indoor_module.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/netatmo_outdoor_module.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/img/wifi_1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/wifi_2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/wifi_3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/img/wifi_4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rpi/cbatmo.service: -------------------------------------------------------------------------------- 1 | # SystemD example script 2 | # This script is used on my RPI installed with ArchLinux 3 | # Modify the path of cbatmo folder in WorkingDirectory option 4 | # Modify the User and group with your username and group 5 | # Don't forget to install yarn on your RPI 6 | 7 | [Unit] 8 | Description=CBatmo WEB server 9 | # Set dependencies to other services (optional) 10 | #After=mongodb.service 11 | 12 | [Service] 13 | # Start the js-file starting the express server 14 | ExecStart=/usr/bin/yarn start 15 | WorkingDirectory=/home/alarm/cbatmo 16 | Restart=always 17 | RestartSec=10 18 | StandardOutput=syslog 19 | StandardError=syslog 20 | SyslogIdentifier=CBatmo 21 | # Change to a non-root user (optional, but recommended) 22 | User=alarm 23 | Group=alarm 24 | # Set environment options 25 | Environment=NODE_ENV=production PORT=3000 26 | 27 | [Install] 28 | WantedBy=multi-user.target 29 | -------------------------------------------------------------------------------- /public/img/4dcef6dff88afbb4d1197b1b9a9eb05d.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "es6", 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react", 22 | "rootDir": "./src" 23 | }, 24 | "include": [ 25 | "./src/" 26 | ], 27 | "exclude": [ 28 | "./deploy/", 29 | "./logs/", 30 | "./node_modules/", 31 | "./public/", 32 | "./rpi/", 33 | "./screenshots/", 34 | "./views/" 35 | ], 36 | "typeRoots": [ 37 | "./src/types", 38 | "./node_modules/@types" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/containers/AppContainer.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import { ThunkDispatch } from "redux-thunk"; 3 | import {ApplicationState} from "../store"; 4 | import App from "../components/App" 5 | 6 | const mapStateToProps = ({ application, netatmo}: ApplicationState) => ({ 7 | isConfigured: application.isConfigured, 8 | orientation: application.orientation, 9 | mobile: application.mobile, 10 | phone: application.phone, 11 | tablet: application.tablet, 12 | available_modules: netatmo.station_data?.available_modules, 13 | number_of_additional_modules: netatmo.station_data?.number_of_additional_modules, 14 | selected_indoor_module: netatmo.selected_indoor_module, 15 | }); 16 | 17 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 18 | 19 | }); 20 | 21 | const AppContainer = connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(App); 25 | 26 | export default AppContainer 27 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface IState { 4 | hasError: boolean 5 | } 6 | 7 | class ErrorBoundary extends React.Component { 8 | public state: IState = { hasError: false }; 9 | 10 | public componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { 11 | // Display fallback UI 12 | this.setState({ hasError: true }); 13 | // You can also log the error to an error reporting service 14 | console.error(error, errorInfo); 15 | } 16 | 17 | public render() { 18 | if (this.state.hasError) { 19 | // You can render any custom fallback UI 20 | return
21 |

Something went wrong !!!

22 |

Please reload the app

23 |
; 24 | } 25 | return this.props.children; 26 | } 27 | } 28 | 29 | export default ErrorBoundary 30 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoInformationContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleNetatmoInformation from "../components/ModuleNetatmoInformation" 5 | 6 | const mapStateToProps = ({ netatmo, application}: ApplicationState) => ({ 7 | station_name: netatmo.station_data?.station_name, 8 | last_status_store: netatmo.station_data?.last_status_store, 9 | place: netatmo.station_data?.place, 10 | reachable: netatmo.station_data?.reachable, 11 | locale: application.user.lang, 12 | orientation: application.orientation 13 | }); 14 | 15 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 16 | 17 | }); 18 | 19 | const ModuleNetatmoInformationContainer = connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(ModuleNetatmoInformation); 23 | 24 | export default ModuleNetatmoInformationContainer 25 | -------------------------------------------------------------------------------- /src/img/netatmo_wind_module.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/store/application/types.ts: -------------------------------------------------------------------------------- 1 | import {INetatmoUserInformation} from "../../models/NetatmoUserInformation"; 2 | 3 | export enum ApplicationActionTypes { 4 | APP_CONFIGURED = '@@application/APP_CONFIGURED', 5 | USER_INFO = '@@application/USER_INFO', 6 | DEVICE_ORIENTATION = '@@application/DEVICE_ORIENTATION', 7 | } 8 | 9 | export type Orientation = 'portrait' | 'landscape'; 10 | 11 | export interface IApplicationInfoState { 12 | readonly name: string 13 | readonly description: string 14 | readonly version: string 15 | readonly author: string 16 | } 17 | 18 | // The complete state for the store 19 | export interface IApplicationState { 20 | readonly phone?: string 21 | readonly tablet?: string 22 | readonly mobile?: string 23 | readonly orientation?: Orientation 24 | readonly isConfigured: boolean 25 | readonly info: IApplicationInfoState 26 | readonly user: INetatmoUserInformation 27 | readonly loading: boolean 28 | } 29 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, Dispatch, Action, AnyAction } from 'redux'; 2 | import { applicationReducer } from "./application/reducer"; 3 | import { netatmoReducer } from "./netatmo/reducer"; 4 | import {openWeatherReducer} from "./openweather/reducer"; 5 | import { IApplicationState } from "./application/types"; 6 | import { INetatmoState } from "./netatmo/types"; 7 | import {IOpenWeatherState} from "./openweather/types"; 8 | 9 | // The top-level state object. 10 | export interface ApplicationState { 11 | application: IApplicationState, 12 | openweather: IOpenWeatherState, 13 | netatmo: INetatmoState 14 | } 15 | 16 | // Additional props for connected React components. This prop is passed by default with `connect()` 17 | export interface ConnectedReduxProps { 18 | dispatch: Dispatch 19 | } 20 | 21 | export const rootReducer = combineReducers({ 22 | application: applicationReducer, 23 | openweather: openWeatherReducer, 24 | netatmo: netatmoReducer 25 | }); 26 | -------------------------------------------------------------------------------- /src/i18n/fr/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "notifications": { 3 | "loading": "Chargement...", 4 | "configuration_success": "Modules detectes, merci de patienter...", 5 | "configuration_error": "Oops, une erreur s'est produite!", 6 | "not_reachable": "Ce module ne peut pas joindre la station. Controler les batteries..." 7 | }, 8 | "app_info": { 9 | "description": "Netatmo web application pour Raspberry Pi et ecran officel Raspberry", 10 | "version": "Version", 11 | "created_by": "Cree par" 12 | }, 13 | "forecast" : { 14 | "forecast": "Prevision", 15 | "sunrise": "Lever", 16 | "sunset": "Coucher" 17 | }, 18 | "netatmo": { 19 | "barometer": "Barometre", 20 | "time": "Heure", 21 | "graph": "Graph", 22 | "day": "jour", 23 | "month": "mois", 24 | "temperature": "Temperature", 25 | "humidity": "Humidite", 26 | "co2": "CO2", 27 | "pressure": "Pression", 28 | "cumulative": "Cumule", 29 | "rain": "Cumule", 30 | "noise": "Bruit", 31 | "wind_strength": "Force du vent", 32 | "windstrength": "Force du vent", 33 | "wind_max_day": "Max jour" 34 | } 35 | } -------------------------------------------------------------------------------- /src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import {Colors} from "@blueprintjs/core"; 3 | import {DataTypes} from "../types/netatmo"; 4 | 5 | export const momentWithLocale = (locale: string) => { 6 | switch (locale) { 7 | case 'fr': 8 | require('moment/locale/fr'); 9 | moment.locale('fr'); 10 | break; 11 | case 'en': 12 | moment.locale('en'); 13 | break; 14 | case 'de': 15 | require('moment/locale/de'); 16 | moment.locale('de'); 17 | break; 18 | default: 19 | moment.locale('en'); 20 | break; 21 | } 22 | 23 | return moment; 24 | }; 25 | 26 | export const colorChooser = (type: DataTypes): string => { 27 | switch (type) { 28 | case 'Temperature': 29 | return Colors.GOLD5; 30 | case 'Humidity': 31 | return Colors.BLUE5; 32 | case 'CO2': 33 | return Colors.GREEN5; 34 | case 'Noise': 35 | return Colors.RED5; 36 | case 'Pressure': 37 | return Colors.COBALT5; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cedric Bapst 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 | -------------------------------------------------------------------------------- /src/i18n/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "notifications": { 3 | "loading": "Loading...", 4 | "configuration_success": "Configured with success, please wait...", 5 | "configuration_error": "Oops, an error occur!", 6 | "not_reachable": "This module cannot reach the Station. Check the battery status or bring it closer to the Station." 7 | }, 8 | "app_info": { 9 | "description": "A Netatmo Weather Station Web-APP for Raspberry Pi and official Raspberry touchscreen", 10 | "version": "Version", 11 | "created_by": "Created by" 12 | }, 13 | "forecast" : { 14 | "forecast": "Forecast", 15 | "sunrise": "Sunrise", 16 | "sunset": "Sunset" 17 | }, 18 | "netatmo": { 19 | "barometer": "Barometer", 20 | "time": "Time", 21 | "graph": "Graph", 22 | "day": "day", 23 | "month": "month", 24 | "temperature": "Temperature", 25 | "humidity": "Humidity", 26 | "co2": "CO2", 27 | "pressure": "Pressure", 28 | "cumulative": "Cumulative", 29 | "rain": "Cumulative", 30 | "noise": "Noise", 31 | "wind_strength": "Wind strength", 32 | "windstrength": "Wind strength", 33 | "wind_max_day": "Max day" 34 | } 35 | } -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoWindContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleNetatmoWind from "../components/ModuleNetatmoWind" 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {Timelapse} from "../types/netatmo"; 7 | 8 | const mapStateToProps = ({ netatmo, application}: ApplicationState) => ({ 9 | module_data: netatmo.station_data?.modules.WIND, 10 | device_id: netatmo.station_data?.id, 11 | unit: application.user.windunit, 12 | selected_timelapse: netatmo.selected_timelapse, 13 | wind_ratio: application.user.wind_ratio, 14 | orientation: application.orientation, 15 | }); 16 | 17 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 18 | fetchMeasure: (device: string, module: string, type: string[], timelapse: Timelapse) => dispatch(netatmoActions.fetchMeasure(device, module, type, timelapse)) 19 | }); 20 | 21 | const ModuleNetatmoWindContainer = connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(ModuleNetatmoWind); 25 | 26 | export default ModuleNetatmoWindContainer 27 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoRainContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleNetatmoRain from "../components/ModuleNetatmoRain" 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {Timelapse} from "../types/netatmo"; 7 | 8 | const mapStateToProps = ({ netatmo, application }: ApplicationState) => ({ 9 | module_data: netatmo.station_data?.modules.RAIN, 10 | device_id: netatmo.station_data?.id, 11 | selected_timelapse: netatmo.selected_timelapse, 12 | distance_unit: application.user.distance_unit, 13 | rain_ratio: application.user.rain_ratio, 14 | orientation: application.orientation, 15 | }); 16 | 17 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 18 | fetchMeasure: (device: string, module: string, type: string[], timelapse: Timelapse) => dispatch(netatmoActions.fetchMeasure(device, module, type, timelapse)) 19 | }); 20 | 21 | const ModuleNetatmoRainContainer = connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(ModuleNetatmoRain); 25 | 26 | export default ModuleNetatmoRainContainer 27 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoNotReachable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import {Icon, Intent} from "@blueprintjs/core"; 4 | import { withTranslation, WithTranslation } from 'react-i18next'; 5 | import * as i18next from 'i18next'; 6 | 7 | // Separate state props + dispatch props to their own interfaces. 8 | interface IPropsFromState extends WithTranslation { 9 | last_seen?: number 10 | t: i18next.TFunction 11 | } 12 | 13 | const NetatmoModuleError: React.FunctionComponent = (props) => { 14 | return ( 15 |
16 | 17 |
18 |
19 | {props.t('notifications.not_reachable')} 20 |
21 | { 22 | props.last_seen ? ( 23 |
Last seen : {moment.unix(props.last_seen).format('DD.MM.YYYY HH:mm')}.
24 | ) : null 25 | } 26 |
27 |
28 | ) 29 | }; 30 | 31 | export default withTranslation('common')(NetatmoModuleError); 32 | -------------------------------------------------------------------------------- /src/i18n/de/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "notifications": { 3 | "loading": "Laedt...", 4 | "configuration_success": "Konfiguration abgeschlossen, bitte warten...", 5 | "configuration_error": "Oops, es ist ein Fehler aufgetreten!", 6 | "not_reachable": "Dieses Modul kann die Station nicht erreichen. Ueberpruefen Sie den Batteriestatus oder bringen Sie es naeher an die Station." 7 | }, 8 | "app_info": { 9 | "description": "Netatmo Wetterstation fuer Raspberry Pi mit offiziellem Touchscreen.", 10 | "version": "Version", 11 | "created_by": "Erstellt von" 12 | }, 13 | "forecast" : { 14 | "forecast": "Vorhersage", 15 | "sunrise": "Sonnenaufgang", 16 | "sunset": "Sonnenuntergang" 17 | }, 18 | "netatmo": { 19 | "barometer": "Barometer", 20 | "time": "Zeit", 21 | "graph": "Graph", 22 | "day": "Tag", 23 | "month": "Monat", 24 | "temperature": "Temperatur", 25 | "humidity": "Luftfeuchtigkeit", 26 | "co2": "CO2", 27 | "pressure": "Druck", 28 | "cumulative": "Kumulativ", 29 | "rain": "Kumulativ", 30 | "noise": "Laerm", 31 | "wind_strength": "Windgeschwindigkeit", 32 | "windstrength": "Windgeschwindigkeit", 33 | "wind_max_day": "Max Tag" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoGraphContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch } from "redux-thunk"; 3 | import ModuleNetatmoGraph from "../components/ModuleNetatmoGraph"; 4 | import {ApplicationState} from "../store"; 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {Timelapse} from "../types/netatmo"; 7 | 8 | const mapStateToProps = ({ netatmo, application }: ApplicationState) => ({ 9 | measure_data: netatmo.measure_data, 10 | selected_types: netatmo.selected_types, 11 | selected_module: netatmo.selected_module, 12 | selected_timelapse: netatmo.selected_timelapse, 13 | station_data: netatmo.station_data, 14 | phone: application.phone, 15 | mobile: application.mobile, 16 | orientation: application.orientation 17 | }); 18 | 19 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 20 | fetchMeasure: (device: string, module: string, type: string[], timelapse: Timelapse) => dispatch(netatmoActions.fetchMeasure(device, module, type, timelapse)), 21 | }); 22 | 23 | const ModuleNetatmoRainGraphContainer = connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(ModuleNetatmoGraph); 27 | 28 | export default ModuleNetatmoRainGraphContainer 29 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoBarometerContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleNetatmoBarometer from "../components/ModuleNetatmoBarometer" 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {Timelapse} from "../types/netatmo"; 7 | 8 | const mapStateToProps = ({ netatmo, application}: ApplicationState) => ({ 9 | reachable: netatmo.station_data?.reachable, 10 | pressure: netatmo.station_data?.data?.pressure, 11 | pressure_unit: application.user.pressure_unit, 12 | device_id: netatmo.station_data?.id, 13 | selected_timelapse: netatmo.selected_timelapse, 14 | pressure_ratio: application.user.pressure_ratio, 15 | orientation: application.orientation, 16 | }); 17 | 18 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 19 | fetchMeasure: (device: string, module: string, type: string[], timelapse: Timelapse) => dispatch(netatmoActions.fetchMeasure(device, module, type, timelapse)) 20 | }); 21 | 22 | const ModuleNetatmoBarometerContainer = connect( 23 | mapStateToProps, 24 | mapDispatchToProps 25 | )(ModuleNetatmoBarometer); 26 | 27 | export default ModuleNetatmoBarometerContainer 28 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, {RefObject} from 'react'; 2 | import {Intent, Position, Toaster} from "@blueprintjs/core"; 3 | import { Flex } from 'reflexbox' 4 | 5 | export const ContextMainLayout = React.createContext(undefined); 6 | ContextMainLayout.displayName = 'MainLayout'; 7 | 8 | class MainLayout extends React.Component { 9 | private toaster: Toaster | undefined; 10 | 11 | private refHandlers = { 12 | toaster: (ref: Toaster) => (this.toaster = ref), 13 | }; 14 | 15 | private addToast = (icon: any, msg: string, intent: Intent, timeout: number = 4000) => { 16 | // @ts-ignore 17 | this.toaster.show({icon: icon, message: msg, timeout: timeout, intent: intent}); 18 | }; 19 | 20 | public render() { 21 | return ( 22 | 23 | 29 | {this.props.children} 30 | 31 | 32 | 33 | ); 34 | } 35 | } 36 | 37 | export default MainLayout 38 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoStationContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleNetatmoStation from "../components/ModuleNetatmoStation" 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {DataTypes, Timelapse} from "../types/netatmo"; 7 | 8 | const mapStateToProps = ({ netatmo, application}: ApplicationState) => ({ 9 | station_data: netatmo.station_data, 10 | selected_timelapse: netatmo.selected_timelapse, 11 | temperature_unit: application.user.temperature_unit, 12 | pressure_unit: application.user.pressure_unit, 13 | orientation: application.orientation, 14 | selected_type: netatmo.selected_station_type, 15 | measure_data: netatmo.measure_station_data 16 | }); 17 | 18 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 19 | fetchMeasure: (device: string, module: string, type: string[], timelapse: Timelapse) => dispatch(netatmoActions.fetchMeasure(device, module, type, timelapse)), 20 | onChangeSelectedType: (type: DataTypes, module: string) => dispatch(netatmoActions.onChangeSelectedType(type, module)), 21 | }); 22 | 23 | const ModuleNetatmoStationContainer = connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(ModuleNetatmoStation); 27 | 28 | export default ModuleNetatmoStationContainer 29 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render } from "react-dom"; 3 | import { Provider } from 'react-redux'; 4 | import { initReactI18next } from 'react-i18next'; 5 | import i18next from 'i18next'; 6 | import AppContainer from './containers/AppContainer'; 7 | 8 | import './css/style.scss'; 9 | 10 | /** i18n translations **/ 11 | import common_en from './i18n/en/common.json'; 12 | import common_fr from './i18n/fr/common.json'; 13 | import common_de from './i18n/de/common.json'; 14 | 15 | i18next 16 | .use(initReactI18next) 17 | .init({ 18 | interpolation: { escapeValue: false }, 19 | debug: true, 20 | resources: { 21 | en: { common: common_en }, 22 | fr: { common: common_fr }, 23 | de: { common: common_de } 24 | }, 25 | lng: window.localStorage.getItem('locale') ? window.localStorage.getItem('locale') as string : 'en', 26 | fallbackLng: 'en', 27 | ns: ['common'], 28 | defaultNS: 'common' 29 | }); 30 | 31 | /** Register Redux store */ 32 | const initialState: any = JSON.parse(window.preloadedState) || {}; 33 | import configureStore from './configureStore'; 34 | const store = configureStore(initialState); 35 | 36 | render( 37 | 38 | 39 | , 40 | document.getElementById('app-root') as HTMLElement 41 | ); 42 | -------------------------------------------------------------------------------- /src/containers/AppStartingContainer.ts: -------------------------------------------------------------------------------- 1 | import {connect} from 'react-redux' 2 | import { ThunkDispatch } from "redux-thunk"; 3 | import * as netatmoActions from "../store/netatmo/actions"; 4 | import * as applicationActions from "../store/application/actions"; 5 | import { ApplicationState } from "../store"; 6 | import AppStarting from "../components/AppStarting" 7 | 8 | const mapStateToProps = ({ application, netatmo}: ApplicationState) => ({ 9 | loading_auth: netatmo.loading_auth, 10 | loading_station_data: netatmo.loading_station_data, 11 | mobile: application.mobile, 12 | tablet: application.tablet, 13 | phone: application.phone, 14 | info: application.info, 15 | user: application.user, 16 | station_data_errors: netatmo.station_data_errors, 17 | refresh_token: netatmo.refresh_token, 18 | access_token: netatmo.access_token, 19 | }); 20 | 21 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 22 | fetchAuth: (username: string, password: string, secret: string) => dispatch(netatmoActions.fetchAuth(username, password, secret)), 23 | fetchStationData: () => dispatch(netatmoActions.fetchStationData()), 24 | appConfigured: (value: boolean) => dispatch(applicationActions.appConfigured(value)) 25 | }); 26 | 27 | const AppStartingContainer = connect( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(AppStarting); 31 | 32 | export default AppStartingContainer 33 | -------------------------------------------------------------------------------- /src/store/openweather/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from "redux"; 2 | import { OpenWeatherActionTypes, IOpenWeatherState } from "./types"; 3 | 4 | const initialState: IOpenWeatherState = { 5 | loading: true, 6 | first_fetch: true, 7 | data: undefined, 8 | updated_at: 0, 9 | errors: undefined 10 | }; 11 | 12 | const reducer: Reducer = (state = initialState, action) => { 13 | if (typeof state === 'undefined') { 14 | // No preloadedState from server. Use local state. 15 | state = { ...initialState } 16 | } else { 17 | // PreloadedState supplied by the server, but it's not merged with our local initial state yet. 18 | state = { ...initialState, ...state } 19 | } 20 | 21 | switch (action.type) { 22 | case OpenWeatherActionTypes.REQUEST: 23 | return { ...state, loading: true }; 24 | 25 | case OpenWeatherActionTypes.SUCCESS: 26 | return { ...state, 27 | loading: false, 28 | data: action.payload, 29 | updated_at: action.receivedAt , 30 | errors: undefined, 31 | first_fetch: false 32 | }; 33 | 34 | case OpenWeatherActionTypes.FAILURE: 35 | return { ...state, loading: false, errors: action.error }; 36 | 37 | default: 38 | return state; 39 | } 40 | }; 41 | 42 | export {reducer as openWeatherReducer} 43 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoOutdoorContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleNetatmoOutdoor from "../components/ModuleNetatmoOutdoor" 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {DataTypes, Timelapse} from "../types/netatmo"; 7 | 8 | const mapStateToProps = ({ netatmo, application}: ApplicationState) => ({ 9 | module_data: netatmo.station_data?.modules.OUTDOOR, 10 | device_id: netatmo.station_data?.id, 11 | selected_timelapse: netatmo.selected_timelapse, 12 | temperature_ratio: application.user.temperature_ratio, 13 | temperature_unit: application.user.temperature_unit, 14 | orientation: application.orientation, 15 | selected_type: netatmo.selected_outdoor_type, 16 | measure_data: netatmo.measure_outdoor_data 17 | }); 18 | 19 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 20 | fetchMeasure: (device: string, module: string, type: string[], timelapse: Timelapse) => dispatch(netatmoActions.fetchMeasure(device, module, type, timelapse)), 21 | onChangeSelectedType: (type: DataTypes, module: string) => dispatch(netatmoActions.onChangeSelectedType(type, module)), 22 | }); 23 | 24 | const ModuleNetatmoOutdoorContainer = connect( 25 | mapStateToProps, 26 | mapDispatchToProps 27 | )(ModuleNetatmoOutdoor); 28 | 29 | export default ModuleNetatmoOutdoorContainer 30 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoRainGraph.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Colors} from "@blueprintjs/core"; 3 | import {BarChart, Bar, XAxis, YAxis, CartesianGrid, ResponsiveContainer} from 'recharts'; 4 | import {Orientation} from "../store/application/types"; 5 | 6 | // Separate state props + dispatch props to their own interfaces. 7 | interface IPropsFromState { 8 | data: [] 9 | phone?: string 10 | mobile?: string 11 | orientation?: Orientation 12 | } 13 | 14 | const ModuleNetatmoRainGraph: React.FunctionComponent = (props) => { 15 | return( 16 | 17 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | }; 32 | 33 | 34 | export default ModuleNetatmoRainGraph 35 | -------------------------------------------------------------------------------- /src/img/netatmo_rain_module.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoIndoorContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleNetatmoIndoor from "../components/ModuleNetatmoIndoor" 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {DataTypes, Timelapse} from "../types/netatmo"; 7 | 8 | const mapStateToProps = ({ netatmo, application}: ApplicationState) => ({ 9 | module_data: netatmo.station_data?.modules.INDOOR, 10 | device_id: netatmo.station_data?.id, 11 | selected_timelapse: netatmo.selected_timelapse, 12 | temperature_unit: application.user.temperature_unit, 13 | orientation: application.orientation, 14 | selected_type: netatmo.selected_indoor_type, 15 | measure_data: netatmo.measure_indoor_data, 16 | number_of_additional_modules: netatmo.station_data?.number_of_additional_modules, 17 | indoor_module_names: netatmo.station_data?.indoor_module_names, 18 | selected_indoor_module: netatmo.selected_indoor_module, 19 | }); 20 | 21 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 22 | fetchMeasure: (device: string, module: string, type: string[], timelapse: Timelapse) => dispatch(netatmoActions.fetchMeasure(device, module, type, timelapse)), 23 | onChangeSelectedType: (type: DataTypes, module: string) => dispatch(netatmoActions.onChangeSelectedType(type, module)), 24 | onChangeSelectedInsideModule: (module: number) => dispatch(netatmoActions.onChangeSelectedInsideModule(module)), 25 | }); 26 | 27 | const ModuleNetatmoIndoorContainer = connect( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(ModuleNetatmoIndoor); 31 | 32 | export default ModuleNetatmoIndoorContainer 33 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoIndoorThirdContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleNetatmoIndoor from "../components/ModuleNetatmoIndoor" 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {DataTypes, Timelapse} from "../types/netatmo"; 7 | 8 | const mapStateToProps = ({ netatmo, application}: ApplicationState) => ({ 9 | module_data: netatmo.station_data?.modules.INDOOR_THIRD, 10 | device_id: netatmo.station_data?.id, 11 | selected_timelapse: netatmo.selected_timelapse, 12 | temperature_unit: application.user.temperature_unit, 13 | orientation: application.orientation, 14 | selected_type: netatmo.selected_indoor_third_type, 15 | measure_data: netatmo.measure_indoor_third_data, 16 | number_of_additional_modules: netatmo.station_data?.number_of_additional_modules, 17 | indoor_module_names: netatmo.station_data?.indoor_module_names, 18 | selected_indoor_module: netatmo.selected_indoor_module, 19 | }); 20 | 21 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 22 | fetchMeasure: (device: string, module: string, type: string[], timelapse: Timelapse) => dispatch(netatmoActions.fetchMeasure(device, module, type, timelapse)), 23 | onChangeSelectedType: (type: DataTypes, module: string) => dispatch(netatmoActions.onChangeSelectedType(type, module)), 24 | onChangeSelectedInsideModule: (module: number) => dispatch(netatmoActions.onChangeSelectedInsideModule(module)), 25 | }); 26 | 27 | const ModuleNetatmoIndoorThirdContainer = connect( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(ModuleNetatmoIndoor); 31 | 32 | export default ModuleNetatmoIndoorThirdContainer 33 | -------------------------------------------------------------------------------- /src/containers/ModuleNetatmoIndoorSecondContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch} from "redux-thunk"; 3 | import { ApplicationState } from "../store"; 4 | import ModuleNetatmoIndoor from "../components/ModuleNetatmoIndoor" 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {DataTypes, Timelapse} from "../types/netatmo"; 7 | 8 | const mapStateToProps = ({ netatmo, application}: ApplicationState) => ({ 9 | module_data: netatmo.station_data?.modules.INDOOR_SECOND, 10 | device_id: netatmo.station_data?.id, 11 | selected_timelapse: netatmo.selected_timelapse, 12 | temperature_unit: application.user.temperature_unit, 13 | orientation: application.orientation, 14 | selected_type: netatmo.selected_indoor_second_type, 15 | measure_data: netatmo.measure_indoor_second_data, 16 | number_of_additional_modules: netatmo.station_data?.number_of_additional_modules, 17 | indoor_module_names: netatmo.station_data?.indoor_module_names, 18 | selected_indoor_module: netatmo.selected_indoor_module, 19 | }); 20 | 21 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 22 | fetchMeasure: (device: string, module: string, type: string[], timelapse: Timelapse) => dispatch(netatmoActions.fetchMeasure(device, module, type, timelapse)), 23 | onChangeSelectedType: (type: DataTypes, module: string) => dispatch(netatmoActions.onChangeSelectedType(type, module)), 24 | onChangeSelectedInsideModule: (module: number) => dispatch(netatmoActions.onChangeSelectedInsideModule(module)), 25 | }); 26 | 27 | const ModuleNetatmoIndoorSecondContainer = connect( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(ModuleNetatmoIndoor); 31 | 32 | export default ModuleNetatmoIndoorSecondContainer 33 | -------------------------------------------------------------------------------- /src/models/NetatmoChartsData.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import {INetatmoUserInformation} from "./NetatmoUserInformation"; 3 | 4 | export interface INetatmoChartData { 5 | data: any[] 6 | } 7 | 8 | /** Netatmo Charts Data model */ 9 | class NetatmoModuleChartData implements INetatmoChartData { 10 | data = []; 11 | 12 | constructor(data: any, type: string[], userInfo: INetatmoUserInformation) { 13 | this.data = []; 14 | 15 | Object.entries(data).map((obj: any) => { 16 | const formatedObject: any = {name: moment.unix(Number.parseInt(obj[0])).format('HH:mm')}; 17 | 18 | type.map((label, index) => { 19 | // Convert value according to user units 20 | switch (label) { 21 | case 'Temperature': 22 | formatedObject[label] = Math.round(eval(obj[1][index] + '*' + userInfo.temperature_ratio) * 10) / 10; 23 | break; 24 | case 'Pressure': 25 | formatedObject[label] = Math.round(obj[1][index] * userInfo.pressure_ratio); 26 | break; 27 | case 'Rain': 28 | formatedObject[label] = Math.round(eval(obj[1][index] + '* 10 *' + userInfo.temperature_ratio) * 10) / 10; 29 | break; 30 | case 'windStrength': 31 | formatedObject[label] = Math.round(obj[1][index] * userInfo.wind_ratio); 32 | break; 33 | default: 34 | formatedObject[label] = obj[1][index]; 35 | break; 36 | } 37 | 38 | }); 39 | 40 | // @ts-ignore 41 | this.data.push(formatedObject) 42 | }); 43 | 44 | console.debug(this) 45 | } 46 | } 47 | 48 | export default NetatmoModuleChartData 49 | -------------------------------------------------------------------------------- /src/store/application/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { IApplicationState, ApplicationActionTypes } from "./types"; 3 | import MobileDetect from 'mobile-detect'; 4 | 5 | const md = new MobileDetect(window.navigator.userAgent); 6 | 7 | const initialState: IApplicationState = { 8 | phone: md.phone(), 9 | tablet: md.tablet(), 10 | mobile: md.mobile(), 11 | orientation: 'landscape', 12 | isConfigured: false, 13 | info: { 14 | name: '', 15 | description: '', 16 | version: '', 17 | author: '', 18 | }, 19 | user: { 20 | mail: '', 21 | lang: '', 22 | locale: '', 23 | pressure_unit: '', 24 | unit: '', 25 | temperature_unit: '', 26 | distance_unit: '', 27 | windunit: '', 28 | temperature_ratio: '', 29 | pressure_ratio: 1, 30 | wind_ratio: 1, 31 | rain_ratio: 1 32 | }, 33 | loading: true 34 | }; 35 | 36 | const reducer: Reducer = (state = initialState, action) => { 37 | if (typeof state === 'undefined') { 38 | // No preloadedState from server. Use local state. 39 | state = { ...initialState } 40 | } else { 41 | // PreloadedState supplied by the server, but it's not merged with our local initial state yet. 42 | state = { ...initialState, ...state } 43 | } 44 | 45 | switch (action.type) { 46 | case ApplicationActionTypes.APP_CONFIGURED: 47 | return { ...state, isConfigured: action.payload }; 48 | 49 | /** SET user info from Netatmo API **/ 50 | case ApplicationActionTypes.USER_INFO: 51 | return { ...state, user: action.payload }; 52 | 53 | case ApplicationActionTypes.DEVICE_ORIENTATION: 54 | return { ...state, orientation: action.payload }; 55 | 56 | default: 57 | return state; 58 | } 59 | }; 60 | 61 | export {reducer as applicationReducer} 62 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | <%= title %>
-------------------------------------------------------------------------------- /src/components/ModuleForecastDaily.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import removeAccents from 'remove-accents'; 3 | import { Colors } from "@blueprintjs/core"; 4 | import cx from 'classnames'; 5 | 6 | import WeatherIcon from "./WeatherIcon"; 7 | import { momentWithLocale } from '../utils/tools' 8 | import { IDailyData } from "../models/WeatherInterface"; 9 | 10 | interface IPropsFromState { 11 | data: IDailyData|undefined 12 | locale: string 13 | phone?: string 14 | } 15 | 16 | const ModuleForecastDaily: React.FunctionComponent = ({data, locale, phone}) => { 17 | let moment = momentWithLocale(locale); 18 | 19 | return ( 20 |
21 |
22 | { data ? (moment.unix(data.time).format('dddd')) : 'Monday' } 23 |
24 | { 25 | !phone ? ( 26 |
27 |
31 | {data && removeAccents(momentWithLocale(locale).unix(data?.time).format('DD MMM'))} 32 |
33 |
34 | ) : null 35 | } 36 |
37 |
38 | { data ? data.temperature_low.toFixed(0) : '0.0'}° 39 |
40 |
41 | {data ? data.temperature_high.toFixed(0) : '0.0'}° 42 |
43 |
44 |
45 | 46 |
47 |
48 | ) 49 | }; 50 | 51 | export default ModuleForecastDaily; 52 | -------------------------------------------------------------------------------- /src/models/DarkskyData.ts: -------------------------------------------------------------------------------- 1 | /** Darksky Data Mode */ 2 | import {ICurrently, IDaily, IDailyData, IPlace} from "./WeatherInterface"; 3 | 4 | export interface IDarkskyData { 5 | last_status_store: number; 6 | place: IPlace; 7 | currently: ICurrently; 8 | daily: IDaily; 9 | } 10 | 11 | /** 12 | * @deprecated Replaced by OpenWeatherData 13 | */ 14 | class DarkskyData implements IDarkskyData { 15 | last_status_store: number; 16 | place: IPlace; 17 | currently: ICurrently; 18 | daily: IDaily; 19 | 20 | constructor(data: any) { 21 | this.last_status_store = data.currently.time; 22 | this.place = { 23 | timezone: data.timezone, 24 | latitude: data.latitude, 25 | longitude: data.longitude, 26 | }; 27 | this.currently = { 28 | time: data.currently.time, 29 | temperature: data.currently.temperature, 30 | humidity: data.currently.humidity, 31 | pressure: data.currently.pressure, 32 | wind_speed: data.currently.windSpeed, 33 | wind_gust: data.currently.windGust, 34 | uv_index: data.currently.uvIndex, 35 | ozone: data.currently.ozone, 36 | icon: data.currently.icon, 37 | summary: data.currently.summary, 38 | }; 39 | 40 | this.daily = { 41 | summary: data.daily.summary, 42 | icon: data.daily.icon, 43 | data: [] 44 | }; 45 | 46 | data.daily.data.map((day: any) => { 47 | const dataDay: IDailyData = { 48 | time: day.time, 49 | summary: day.summary, 50 | icon: day.icon, 51 | sunrise_time: day.sunriseTime, 52 | sunset_time: day.sunsetTime, 53 | moon_phase: day.moonPhase, 54 | temperature_low: day.temperatureLow, 55 | temperature_high: day.temperatureHigh, 56 | }; 57 | this.daily.data.push(dataDay) 58 | }); 59 | 60 | console.debug(this) 61 | } 62 | } 63 | 64 | export default DarkskyData 65 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= title %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 |
34 | 35 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoBarometer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withTranslation, WithTranslation } from 'react-i18next'; 3 | import * as i18next from 'i18next'; 4 | import ModuleLayout from "../layouts/ModuleLayout"; 5 | import * as netatmoActions from "../store/netatmo/actions"; 6 | import {ConnectedReduxProps} from "../store"; 7 | import {Orientation} from "../store/application/types"; 8 | import {Timelapse} from "../types/netatmo"; 9 | 10 | // Separate state props + dispatch props to their own interfaces. 11 | interface IPropsFromState { 12 | reachable?: boolean 13 | pressure?: number 14 | pressure_unit: string 15 | device_id: string|undefined 16 | selected_timelapse: Timelapse 17 | pressure_ratio: number 18 | orientation: Orientation 19 | } 20 | 21 | // We can use `typeof` here to map our dispatch types to the props, like so. 22 | interface IPropsFromDispatch extends WithTranslation { 23 | [key: string]: any 24 | fetchMeasure: typeof netatmoActions.fetchMeasure 25 | t: i18next.TFunction 26 | } 27 | 28 | // Combine both state + dispatch props - as well as any props we want to pass - in a union type. 29 | type AllProps = IPropsFromState & IPropsFromDispatch & ConnectedReduxProps; 30 | 31 | /** Outdoor module */ 32 | const NetatmoModuleBarmometer: React.FunctionComponent = (props) => { 33 | return ( 34 | 40 |
41 |
42 |
props.fetchMeasure(props.device_id as string, props.device_id as string, ['Pressure'], props.selected_timelapse)}> 43 | {props.pressure}{props.pressure_unit} 44 |
45 |
46 |
47 |
48 | ) 49 | }; 50 | 51 | export default withTranslation('common')(NetatmoModuleBarmometer) 52 | -------------------------------------------------------------------------------- /src/models/OpenWeatherData.ts: -------------------------------------------------------------------------------- 1 | /** OpenWeather Data Model */ 2 | import {ICurrently, IDaily, IDailyData, IPlace} from "./WeatherInterface"; 3 | 4 | export interface IOpenWeatherData { 5 | last_status_store: number; 6 | place: IPlace; 7 | currently: ICurrently; 8 | daily: IDaily; 9 | } 10 | 11 | class OpenWeatherData implements IOpenWeatherData { 12 | last_status_store: number; 13 | place: IPlace; 14 | currently: ICurrently; 15 | daily: IDaily; 16 | 17 | constructor(data: any) { 18 | console.log(data) 19 | this.last_status_store = data.current.dt; 20 | this.place = { 21 | timezone: data.timezone, 22 | latitude: data.lat, 23 | longitude: data.lon, 24 | }; 25 | this.currently = { 26 | time: data.current.dt, 27 | temperature: data.current.temp, 28 | humidity: data.current.humidity, 29 | pressure: data.current.pressure, 30 | wind_speed: data.current.wind_speed, 31 | wind_gust: data.current.wind_gust, 32 | uv_index: data.current.uvi, 33 | ozone: 0, // not exist in openweather 34 | icon: data.current.weather[0].icon, 35 | summary: data.current.weather[0].description, 36 | }; 37 | 38 | this.daily = { 39 | summary: data.current.weather[0].description, 40 | icon: data.current.weather[0].icon, 41 | data: [] 42 | }; 43 | 44 | data.daily.map((day: any) => { 45 | const dataDay: IDailyData = { 46 | time: day.dt, 47 | summary: day.weather[0].description, 48 | icon: day.weather[0].icon, 49 | icon_id: day.weather[0].id, 50 | sunrise_time: day.sunrise, 51 | sunset_time: day.sunset, 52 | moon_phase: 0, // not exist in openweather 53 | temperature_low: day.temp.min, 54 | temperature_high: day.temp.max, 55 | }; 56 | this.daily.data.push(dataDay) 57 | }); 58 | 59 | console.debug(this) 60 | } 61 | } 62 | 63 | export default OpenWeatherData 64 | -------------------------------------------------------------------------------- /src/layouts/DashboardLayoutContainer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { ThunkDispatch } from "redux-thunk"; 3 | import * as openweatherActions from '../store/openweather/actions' 4 | import * as netatmoActions from '../store/netatmo/actions' 5 | import * as applicationActions from '../store/application/actions' 6 | import DashboardLayout from "./DashboardLayout"; 7 | import { ApplicationState } from "../store"; 8 | import {Orientation} from "../store/application/types"; 9 | import {DataTypes} from "../types/netatmo"; 10 | 11 | const mapStateToProps = ({ netatmo, application }: ApplicationState) => ({ 12 | station_data: netatmo.station_data, 13 | selected_module: netatmo.selected_module, 14 | selected_types: netatmo.selected_types, 15 | selected_station_type: netatmo.selected_station_type, 16 | selected_outdoor_type: netatmo.selected_outdoor_type, 17 | selected_indoor_type: netatmo.selected_indoor_type, 18 | selected_indoor_second_type: netatmo.selected_indoor_second_type, 19 | selected_indoor_third_type: netatmo.selected_indoor_third_type, 20 | selected_timelapse: netatmo.selected_timelapse, 21 | mobile: application.mobile 22 | }); 23 | 24 | const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ 25 | fetchOpenWeather: () => dispatch(openweatherActions.fetchOpenWeather()), 26 | fetchStationData: () => dispatch(netatmoActions.fetchStationData()), 27 | fetchMeasure: (device: string, module: string, types: string[], timelapse: '12h'|'1d'|'1m') => dispatch(netatmoActions.fetchMeasure(device, module, types, timelapse)), 28 | fetchMeasures: (device: string, module: string, types: DataTypes[], timelapse: '12h'|'1d'|'1m', module_name: string) => dispatch(netatmoActions.fetchMeasures(device, module, types, timelapse, module_name)), 29 | fetchRainMeasure: (device: string, module: string) => dispatch(netatmoActions.fetchRainMeasure(device, module)), 30 | setOrientation: (orientation: Orientation) => dispatch(applicationActions.setOrientation(orientation)), 31 | }); 32 | 33 | const DashboardLayoutContainer = connect( 34 | mapStateToProps, 35 | mapDispatchToProps 36 | )(DashboardLayout); 37 | 38 | export default DashboardLayoutContainer 39 | -------------------------------------------------------------------------------- /src/store/openweather/actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'redux' 2 | import { ApplicationState } from '../index' 3 | import { ThunkAction } from 'redux-thunk' 4 | import moment from "moment"; 5 | import { OpenWeatherActionTypes } from "./types"; 6 | import OpenWeatherData from '../../models/OpenWeatherData'; 7 | 8 | const CALL_DELAY = 30; 9 | 10 | export const requestData = () => { 11 | return { 12 | type: OpenWeatherActionTypes.REQUEST 13 | } 14 | }; 15 | 16 | export const successData = (json: any) => { 17 | return { 18 | type: OpenWeatherActionTypes.SUCCESS, 19 | payload: json, 20 | receivedAt: Date.now() 21 | } 22 | }; 23 | 24 | export const failureData = (error: any) => { 25 | return { 26 | type: OpenWeatherActionTypes.FAILURE, 27 | error: error 28 | } 29 | }; 30 | 31 | export const fetchOpenWeather = (): ThunkAction> => { 32 | return (dispatch, getState) => { 33 | if (getState().openweather.updated_at === null || getState().openweather.updated_at !== null && moment(moment()).diff(getState().openweather.updated_at, 'minute') >= CALL_DELAY) { 34 | dispatch(requestData()); 35 | 36 | // Take latitude and longitude from Netatmo station 37 | const lat = getState().netatmo.station_data?.place.latitude; 38 | const lng = getState().netatmo.station_data?.place.longitude; 39 | const locale = getState().application.user.lang; 40 | const unit = getState().application.user.unit; 41 | 42 | return fetch(`/openweather/${lat}/${lng}/${locale}/${unit}`) 43 | .then(response => { 44 | if (!response.ok) throw response; 45 | return response.json() 46 | }) 47 | .then(json => { 48 | const data = new OpenWeatherData(json); 49 | dispatch(successData(data)) 50 | }) 51 | .catch(error => { 52 | console.error(error) 53 | error.json().then((errorMessage: any) => { 54 | dispatch(failureData(errorMessage)) 55 | }) 56 | }); 57 | } else { 58 | console.debug('No new OpenWeather data to fetch') 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [2.3.6] - 2021-01-04 6 | ### Added 7 | - Add colors for status icons 8 | 9 | ### Changed 10 | - Replace status icons 11 | - Bug corrections to show the correct icon according to the status (battery, wifi, radio) 12 | - Remove Netatmo modules icons 13 | - Fix issue #46 and #55 14 | 15 | ## [2.3.5] - 2020-12-22 16 | ### Added 17 | - Create new types folder to centralize types for typescript 18 | 19 | ### Changed 20 | - Minor fixes 21 | - Bump Webpack to 5.x + fix webpack config 22 | - Bump React to 17.x 23 | - Upgrade all other NPM packages to their last version 24 | 25 | ## [2.3.4] - 2020-08-23 26 | ### Added 27 | - Add status icons 28 | - Add ability to choose which inside module to show from a dropdown menu when there is more than one 29 | 30 | ### Changed 31 | - Fix issue #30 32 | 33 | 34 | ## [2.3.3] - 2020-07-03 35 | ### Changed 36 | - Fix issues #26 #41 37 | - The issue 26 where the rain graph make the screen shaking was not finally fix by the version 2.3.2, this new commit should definitively fix this damn issue. 38 | 39 | ## [2.3.2] - 2020-06-01 40 | ### Changed 41 | - Fix issues #31 #38 #35 #33 #32 #26 42 | - You can now install cbatmo directly from raspberry pi without any build process from another computer as the build is now commit in github! 43 | 44 | ## [2.3.1] - 2020-05-30 45 | ### Changed 46 | - Improve mobile portrait layout 47 | - Bug fix, rain graph, fullscreen layout for Raspberry touch screen 48 | 49 | ## [2.3.0] - 2020-05-08 50 | ### Added 51 | - Added German language (thanks to [marcol43](https://github.com/marcol123) for the [pull request](https://github.com/Gulivertx/cbatmo/pull/20)) 52 | - Added support for all indoor modules in Redux store 53 | - Added a mobile layout 54 | 55 | ### Changed 56 | - Improve OpenWeather icons 57 | - Fix bugs to show correct units and convert data for graphics according to the user units 58 | 59 | ## [2.2.x] - 2020-04-30 60 | ### Changed 61 | - Replace Darksky API by OpenWeather API 62 | - Add i18n translation support for French and English languages 63 | 64 | ## [2.1.x] - 2019-12-30 65 | ### Changed 66 | - Move javascript code to typescript 67 | - New webpack configuration which is more easy to handle because using the same commands in Windows / MacOS / Linux (no manual OS variable to set anymore) 68 | - Changelog inconsistency section in Bad Practices 69 | 70 | ### Removed 71 | - Remove virtual-keyboard 72 | -------------------------------------------------------------------------------- /deploy/webkit/browser: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env gjs 2 | 3 | /*! (c) 2018 Andrea Giammarchi - @WebReflection - (ISC) */ 4 | 5 | imports.gi.versions.Gtk = '3.0'; 6 | 7 | (function (Gtk, Gdk, WebKit2) {'use strict'; 8 | Gtk.init(null); 9 | const 10 | Screen = Gdk.Screen.get_default(), 11 | BGCOLOR = param('bgcolor', '255,255,255'), 12 | FULLSCREEN = param('fullscreen', false) !== false, 13 | INSPECTOR = param('inspector', false) !== false, 14 | THEME = param('theme', 'Adwaita:dark'), 15 | WIDTH = +param('width', FULLSCREEN ? Screen.get_width() : 480), 16 | HEIGHT = +param('height', FULLSCREEN ? Screen.get_height() : 320), 17 | window = new Gtk.Window({ 18 | title: 'GJS Unframed Browser', 19 | type : Gtk.WindowType.TOPLEVEL, 20 | decorated: false, 21 | window_position: Gtk.WindowPosition.CENTER 22 | }), 23 | webView = new WebKit2.WebView(), 24 | wvSettings = webView.get_settings(), 25 | gtkSettings = Gtk.Settings.get_default(); 26 | ; 27 | 28 | const bgColor = (/^\d+,\d+,\d+$/.test(BGCOLOR) ? 'rgb' : 'rgba') + '(' + BGCOLOR + ')'; 29 | const bg = new Gdk.RGBA(); 30 | bg.parse(bgColor); 31 | webView.set_background_color(bg); 32 | window.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse(bgColor)[1]); 33 | window.set_default_size(WIDTH, HEIGHT); 34 | 35 | const theme = THEME.split(':'); 36 | gtkSettings.gtk_theme_name = theme[0]; 37 | if (/dark/.test(theme[1])) 38 | gtkSettings.gtk_application_prefer_dark_theme = true; 39 | 40 | [].concat( 41 | INSPECTOR ? ['enable_developer_extras'] : [], 42 | [ 43 | 'enable_webgl', 44 | 'enable_webaudio', 45 | 'enable_accelerated_2d_canvas', 46 | 'enable_accelerated_compositing' 47 | ] 48 | ).forEach(function (key) { 49 | wvSettings[key] = true; 50 | }); 51 | 52 | webView.load_uri(url(ARGV.filter(url => '-' !== url[0])[0] || 'archibold.io/test/css/')); 53 | window.connect('show', () => { 54 | if (FULLSCREEN) window.fullscreen(); 55 | Gtk.main(); 56 | }); 57 | window.connect('destroy', () => Gtk.main_quit()); 58 | window.connect('delete_event', () => false); 59 | window.add(webView); 60 | window.show_all(); 61 | 62 | function url(href) { 63 | return /^([a-z]{2,}):/.test(href) ? href : ('http://' + href); 64 | } 65 | 66 | function param(name, value) { 67 | const re = new RegExp('^--' + name + '(=.*)?$', 'i'); 68 | return ARGV.some(p => re.test(p)) ? RegExp.$1.slice(1) : value; 69 | } 70 | 71 | }( 72 | imports.gi.Gtk, 73 | imports.gi.Gdk, 74 | imports.gi.WebKit2 75 | )); 76 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoInformation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Colors } from "@blueprintjs/core"; 3 | import removeAccents from 'remove-accents'; 4 | import { Flex } from 'reflexbox' 5 | import { IPlace } from "../models/NetatmoNAMain"; 6 | import { momentWithLocale } from "../utils/tools"; 7 | import ModuleLayout from "../layouts/ModuleLayout"; 8 | import {Orientation} from "../store/application/types"; 9 | 10 | interface IPropsFromState { 11 | station_name?: string 12 | last_status_store?: number 13 | place?: IPlace 14 | reachable?: boolean 15 | locale: string 16 | orientation?: Orientation 17 | } 18 | 19 | interface IState { 20 | last_status_store: string 21 | } 22 | 23 | class ModuleNetatmoInformation extends React.Component { 24 | private interval: number | undefined; 25 | 26 | public state = { 27 | last_status_store: '' 28 | }; 29 | 30 | public componentDidMount(): void { 31 | this.lastStatusStore(); 32 | 33 | this.interval = window.setInterval(() => { 34 | this.lastStatusStore() 35 | }, 60000); 36 | } 37 | 38 | public componentWillUnmount(): void { 39 | clearInterval(this.interval); 40 | } 41 | 42 | public componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any) { 43 | if (prevProps.last_status_store !== this.props.last_status_store) { 44 | this.lastStatusStore(); 45 | } 46 | } 47 | 48 | private lastStatusStore = () => { 49 | let moment = momentWithLocale(this.props.locale); 50 | const now = moment(); 51 | const updateDate = moment.unix(this.props.last_status_store ? this.props.last_status_store : 0); 52 | this.setState({last_status_store: moment.duration(now.diff(updateDate)).humanize()}); 53 | }; 54 | 55 | public render() { 56 | return ( 57 | 58 | 59 | {this.props.orientation === 'landscape' &&
{this.state.last_status_store}
} 60 |
{removeAccents(this.props.place ? this.props.place.city : '')} - {this.props.place?.altitude}m
61 | {this.props.orientation === 'portrait' &&
{this.state.last_status_store}
} 62 |
63 |
64 | ) 65 | } 66 | } 67 | 68 | export default ModuleNetatmoInformation 69 | -------------------------------------------------------------------------------- /src/components/WeatherIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | 4 | interface IPropsFromState { 5 | condition?: number 6 | size?: number 7 | } 8 | 9 | const WeatherIcon: React.FunctionComponent = ({condition, size = 1}) => { 10 | const setIcon = (condition?: number) => { 11 | /* OpenWeather mapping */ 12 | switch (condition) { 13 | case 200: 14 | case 201: 15 | case 202: 16 | case 210: 17 | case 211: 18 | case 212: 19 | case 221: 20 | case 230: 21 | case 231: 22 | case 232: return 'wi-thunderstorm'; 23 | case 300: 24 | case 301: 25 | case 302: 26 | case 310: 27 | case 311: return 'wi-sprinkle'; 28 | case 312: 29 | case 313: 30 | case 314: 31 | case 321: return 'wi-dust'; 32 | case 500: return 'wi-day-sprinkle'; 33 | case 501: return 'wi-day-hail'; 34 | case 502: return 'wi-rain'; 35 | case 503: return 'wi-rain'; 36 | case 504: return 'wi-rain'; 37 | case 511: return 'wi-rain-mix'; 38 | case 520: return 'wi-day-showers'; 39 | case 521: return 'wi-showers'; 40 | case 522: return 'wi-showers'; 41 | case 531: return 'wi-showers'; 42 | case 600: return 'wi-day-snow'; 43 | case 601: return 'wi-snow'; 44 | case 602: return 'wi-snow'; 45 | case 611: return 'wi-sleet'; 46 | case 612: return 'wi-day-sleet'; 47 | case 613: return 'wi-sleet'; 48 | case 615: return 'wi-rain-mix'; 49 | case 616: return 'wi-rain-mix'; 50 | case 620: return 'wi-day-snow'; 51 | case 621: return 'wi-snow'; 52 | case 622: return 'wi-snow'; 53 | case 701: return 'wi-fog'; 54 | case 711: return 'wi-smoke'; 55 | case 721: return 'wi-fog'; 56 | case 731: return 'wi-dust'; 57 | case 741: return 'wi-fog'; 58 | case 751: return 'wi-sandstorm'; 59 | case 761: return 'wi-dust'; 60 | case 762: return 'wi-volcano'; 61 | case 771: return 'wi-strong-wind'; 62 | case 781: return 'wi-fog'; 63 | case 800: return 'wi-day-sunny'; 64 | case 801: return 'wi-day-cloudy'; 65 | case 802: return 'wi-cloud'; 66 | case 803: return 'wi-cloudy'; 67 | case 804: return 'wi-cloudy'; 68 | default: return ''; 69 | } 70 | }; 71 | 72 | return ( 73 | 74 | ) 75 | }; 76 | 77 | export default WeatherIcon; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cbatmo", 3 | "version": "2.3.6", 4 | "description": "A Netatmo Weather Station Web-APP for Raspberry Pi and official Raspberry touchscreen", 5 | "keywords": [ 6 | "dark sky", 7 | "netatmo", 8 | "weather-station", 9 | "webapp" 10 | ], 11 | "main": "index.js", 12 | "repository": "git@github.com:Gulivertx/cbatmo.git", 13 | "author": "Cédric Bapst ", 14 | "license": "MIT", 15 | "private": false, 16 | "scripts": { 17 | "start": "node server.js", 18 | "build": "webpack", 19 | "watch": "webpack --watch" 20 | }, 21 | "dependencies": { 22 | "body-parser": "^1.18.3", 23 | "body-scroll-lock": "^3.0.2", 24 | "compression": "^1.7.3", 25 | "cookie-parser": "^1.4.4", 26 | "cors": "^2.8.4", 27 | "debug": "^4.1.1", 28 | "dotenv": "^8.2.0", 29 | "ejs": "^3.0.1", 30 | "express": "^4.16.4", 31 | "http-errors": "^1.7.1", 32 | "morgan": "^1.9.1", 33 | "remove": "^0.1.5", 34 | "request": "^2.88.2", 35 | "request-promise": "^4.2.5", 36 | "rotating-file-stream": "^2.0.0" 37 | }, 38 | "devDependencies": { 39 | "@blueprintjs/core": "^3.22.2", 40 | "@mdi/font": "^5.3.45", 41 | "@types/body-scroll-lock": "^2.6.1", 42 | "@types/classnames": "^2.2.9", 43 | "@types/express": "^4.16.1", 44 | "@types/node": "^14.0.9", 45 | "@types/react": "^17.0.0", 46 | "@types/react-dom": "^17.0.0", 47 | "@types/react-redux": "^7.1.5", 48 | "@types/recharts": "^1.8.3", 49 | "@types/reflexbox": "^4.0.0", 50 | "@types/styled-components": "^5.0.1", 51 | "awesome-typescript-loader": "^5.2.1", 52 | "classnames": "^2.2.6", 53 | "concurrently": "^5.0.1", 54 | "cross-fetch": "^3.0.1", 55 | "css-loader": "^5.0.1", 56 | "file-loader": "^6.0.0", 57 | "html-loader": "^1.1.0", 58 | "html-webpack-plugin": "^4.3.0", 59 | "i18next": "^19.4.4", 60 | "image-webpack-loader": "^7.0.1", 61 | "mini-css-extract-plugin": "^1.3.3", 62 | "mobile-detect": "^1.4.4", 63 | "moment": "^2.24.0", 64 | "node-sass": "^5.0.0", 65 | "prop-types": "^15.7.2", 66 | "raw-loader": "^4.0.0", 67 | "react": "^17.0.1", 68 | "react-dom": "^17.0.1", 69 | "react-i18next": "^11.4.0", 70 | "react-redux": "^7.1.3", 71 | "reboot.css": "^1.0.4", 72 | "recharts": "^2.0.0-beta.1", 73 | "redux": "^4.0.1", 74 | "redux-devtools-extension": "^2.13.8", 75 | "redux-thunk": "^2.3.0", 76 | "reflexbox": "^4.0.6", 77 | "remove-accents": "^0.4.2", 78 | "rimraf": "^3.0.0", 79 | "sass-loader": "^10.1.0", 80 | "source-map-loader": "^1.0.0", 81 | "style-loader": "^2.0.0", 82 | "typescript": "^4.1.3", 83 | "url-loader": "^4.1.0", 84 | "weather-icons2": "^2.0.10", 85 | "webpack": "^5.11.0", 86 | "webpack-bundle-analyzer": "^4.3.0", 87 | "webpack-cli": "^4.2.0", 88 | "webpack-pwa-manifest": "^4.2.0", 89 | "webpack-shell-plugin-next": "^2.0.8" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/components/ModuleForecast.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withTranslation, WithTranslation } from 'react-i18next'; 3 | import * as i18next from 'i18next'; 4 | import {Colors} from "@blueprintjs/core"; 5 | import ModuleForecastDaily from "./ModuleForecastDaily"; 6 | import moment from "moment"; 7 | import {Flex} from "reflexbox"; 8 | import cx from "classnames"; 9 | 10 | import ModuleLayout from "../layouts/ModuleLayout"; 11 | import { IOpenWeatherState } from "../store/openweather/types"; 12 | import { Orientation } from "../store/application/types"; 13 | 14 | // Separate state props + dispatch props to their own interfaces. 15 | interface IPropsFromState extends WithTranslation { 16 | openweather: IOpenWeatherState 17 | locale: string 18 | phone?: string 19 | orientation?: Orientation 20 | t: i18next.TFunction 21 | } 22 | 23 | const ModuleDate: React.FunctionComponent = (props) => { 24 | return ( 25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | { 35 | props.phone && props.orientation === 'portrait' && ( 36 | 37 |
38 |
{props.t('forecast.sunrise')}
39 | {moment.unix(props.openweather.data?.daily.data[0].sunrise_time ? props.openweather.data?.daily.data[0].sunrise_time : 0).format('HH:mm')} 40 |
41 |
42 |
{props.t('forecast.sunset')}
43 | {moment.unix(props.openweather.data?.daily.data[0].sunset_time ? props.openweather.data?.daily.data[0].sunset_time : 0).format('HH:mm')} 44 |
45 |
46 | ) 47 | } 48 |
49 | ) 50 | }; 51 | 52 | export default withTranslation('common')(ModuleDate); 53 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoRain.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Colors } from "@blueprintjs/core"; 3 | import { withTranslation, WithTranslation } from 'react-i18next'; 4 | import * as i18next from 'i18next'; 5 | import {Flex} from 'reflexbox'; 6 | import ModuleNetatmoRainGraphContainer from "../containers/ModuleNetatmoRainGraphContainer"; 7 | import ModuleLayout from "../layouts/ModuleLayout"; 8 | 9 | import { INetatmoNAModule3 } from "../models/NetatmoNAModule3"; 10 | import * as netatmoActions from "../store/netatmo/actions"; 11 | import {ConnectedReduxProps} from "../store"; 12 | import {Orientation} from "../store/application/types"; 13 | 14 | // Separate state props + dispatch props to their own interfaces. 15 | interface IPropsFromState { 16 | module_data: INetatmoNAModule3|undefined 17 | device_id: string|undefined 18 | selected_timelapse: '12h'|'1d'|'1m' 19 | distance_unit: string 20 | rain_ratio: number 21 | orientation: Orientation 22 | } 23 | 24 | // We can use `typeof` here to map our dispatch types to the props, like so. 25 | interface IPropsFromDispatch extends WithTranslation { 26 | [key: string]: any 27 | fetchMeasure: typeof netatmoActions.fetchMeasure 28 | t: i18next.TFunction 29 | } 30 | 31 | // Combine both state + dispatch props - as well as any props we want to pass - in a union type. 32 | type AllProps = IPropsFromState & IPropsFromDispatch & ConnectedReduxProps; 33 | 34 | /** Rain module */ 35 | const NetatmoModuleRain: React.FunctionComponent = (props) => { 36 | return ( 37 | 45 |
46 | 47 |
48 | 49 |
50 | 51 |
props.fetchMeasure(props.device_id as string, props.module_data?.id as string, ['Rain'], props.selected_timelapse)} style={{textAlign: 'right'}}> 52 |
{props.t('netatmo.cumulative')}
53 | {props.module_data?.data?.sum_rain_24}{props.distance_unit} 54 |
55 |
props.fetchMeasure(props.device_id as string, props.module_data?.id as string, ['Rain'], props.selected_timelapse)} style={{textAlign: 'right'}}> 56 |
{props.distance_unit}/h
57 | {props.module_data?.data?.sum_rain_1} 58 |
59 |
60 |
61 |
62 |
63 | ) 64 | }; 65 | 66 | export default withTranslation('common')(NetatmoModuleRain) 67 | -------------------------------------------------------------------------------- /src/store/netatmo/types.ts: -------------------------------------------------------------------------------- 1 | import {INetatmoNAMain} from "../../models/NetatmoNAMain"; 2 | import {DataTypes, Timelapse} from "../../types/netatmo"; 3 | 4 | export enum NetatmoActionTypes { 5 | AUTH_REQUEST = '@@netatmo/AUTH_REQUEST', 6 | AUTH_SUCCESS = '@@netatmo/AUTH_SUCCESS', 7 | AUTH_FAILURE = '@@netatmo/AUTH_FAILURE', 8 | 9 | REFRESH_TOKEN_REQUEST = '@@netatmo/REFRESH_TOKEN_REQUEST', 10 | REFRESH_TOKEN_SUCCESS = '@@netatmo/REFRESH_TOKEN_SUCCESS', 11 | REFRESH_TOKEN_FAILURE = '@@netatmo/REFRESH_TOKEN_FAILURE', 12 | 13 | STATION_DATA_REQUEST = '@@netatmo/STATION_DATA_REQUEST', 14 | STATION_DATA_SUCCESS = '@@netatmo/STATION_DATA_SUCCESS', 15 | STATION_DATA_FAILURE = '@@netatmo/STATION_DATA_FAILURE', 16 | 17 | MEASURE_REQUEST = '@@netatmo/MEASURE_REQUEST', 18 | MEASURE_SUCCESS = '@@netatmo/MEASURE_SUCCESS', 19 | MEASURE_FAILURE = '@@netatmo/MEASURE_FAILURE', 20 | 21 | MEASURE_RAIN_REQUEST = '@@netatmo/MEASURE_RAIN_REQUEST', 22 | MEASURE_RAIN_SUCCESS = '@@netatmo/MEASURE_RAIN_SUCCESS', 23 | MEASURE_RAIN_FAILURE = '@@netatmo/MEASURE_RAIN_FAILURE', 24 | 25 | MEASURES_REQUEST = '@@netatmo/MEASURES_REQUEST', 26 | MEASURES_SUCCESS = '@@netatmo/MEASURES_SUCCESS', 27 | MEASURES_FAILURE = '@@netatmo/MEASURES_FAILURE', 28 | 29 | CHANGE_SELECTED_TYPE = '@@netatmo/CHANGE_SELECTED_TYPE', 30 | 31 | CHANGE_SELECTED_INSIDE_MODULE = '@@netatmo/CHANGE_SELECTED_INSIDE_MODULE' 32 | } 33 | 34 | // The complete state for the store 35 | export interface INetatmoState { 36 | readonly client_id: string 37 | readonly client_secret: string 38 | readonly username: string 39 | readonly password: string 40 | 41 | readonly loading_auth: boolean 42 | readonly loading_refresh_token: boolean 43 | readonly auth_errors: any|undefined 44 | readonly access_token: string|null 45 | readonly refresh_token: string|null 46 | readonly access_token_expire_in: number 47 | 48 | readonly loading_station_data: boolean 49 | readonly station_data_last_updated: number 50 | readonly station_data_errors: any 51 | readonly station_data: INetatmoNAMain|undefined 52 | readonly first_fetch: boolean 53 | 54 | readonly selected_indoor_module: 0|1|2 55 | 56 | readonly loading_measure: boolean 57 | readonly measure_data: [] 58 | readonly measure_errors: any|undefined 59 | readonly selected_module: string 60 | readonly selected_types: DataTypes[] 61 | readonly selected_timelapse: Timelapse 62 | 63 | readonly loading_rain_measure: boolean 64 | readonly measure_rain_data: [] 65 | readonly measure_rain_errors: any|undefined 66 | 67 | readonly loading_indoor_measure: boolean 68 | readonly measure_indoor_data: [] 69 | readonly measure_indoor_errors: any|undefined 70 | readonly selected_indoor_type: DataTypes 71 | 72 | readonly loading_indoor_second_measure: boolean 73 | readonly measure_indoor_second_data: [] 74 | readonly measure_indoor_second_errors: any|undefined 75 | readonly selected_indoor_second_type: DataTypes 76 | 77 | readonly loading_indoor_third_measure: boolean 78 | readonly measure_indoor_third_data: [] 79 | readonly measure_indoor_third_errors: any|undefined 80 | readonly selected_indoor_third_type: DataTypes 81 | 82 | readonly loading_outdoor_measure: boolean 83 | readonly measure_outdoor_data: [] 84 | readonly measure_outdoor_errors: any|undefined 85 | readonly selected_outdoor_type: DataTypes 86 | 87 | readonly loading_station_measure: boolean 88 | readonly measure_station_data: [] 89 | readonly measure_station_errors: any|undefined 90 | readonly selected_station_type: DataTypes 91 | } 92 | -------------------------------------------------------------------------------- /rpi/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # @deprecated -> will be replaced by new script deploy/install.sh 3 | # An automatic installer script for CBATMO 4 | # This script works on MacOS and Linux 5 | 6 | # Move in the project root directory 7 | cd .. 8 | 9 | NAME=cbatmo 10 | VERSION="$(sed -n '3p' < package.json | sed -e 's/^[[:space:]]*//' | cut -d'"' -f 4)" 11 | TAR_NAME="$NAME-$VERSION.tar.gz" 12 | DIRECTOY_NAME="${PWD##*/}" 13 | 14 | echo "CBATMO DEPLOY SCRIPT" 15 | echo "CBATMO VERSION ${VERSION}" 16 | echo "______________________________________________________" 17 | 18 | # Verify if .env file exist 19 | ENV_FILE=".env" 20 | if [ ! -f "$ENV_FILE" ]; then 21 | echo "" 22 | echo "${ENV_FILE} file does not exist!" 23 | 24 | while true; do 25 | read -p "Do you want to create a new one? " yn 26 | case $yn in 27 | [Yy]* ) break;; 28 | [Nn]* ) exit 0;; 29 | * ) echo "Please answer yes or no";; 30 | esac 31 | done 32 | 33 | touch .env 34 | echo "APP_ENV=prod" >> $ENV_FILE 35 | read -p "Please specify your APP secret key: " APP_SECRET 36 | echo "APP_SECRET=${APP_SECRET}" >> $ENV_FILE 37 | read -p "Please specify your OpenWeather API key: " OPENWEATHER_API_KEY 38 | echo "OPENWEATHER_API_KEY=${OPENWEATHER_API_KEY}" >> $ENV_FILE 39 | read -p "Please specify your Netatmo client id: " NETATMO_CLIENT_ID 40 | echo "NETATMO_CLIENT_ID=${NETATMO_CLIENT_ID}" >> $ENV_FILE 41 | read -p "Please specify your Netatmo client secret: " NETATMO_CLIENT_SECRET 42 | echo "NETATMO_CLIENT_SECRET=${NETATMO_CLIENT_SECRET}" >> $ENV_FILE 43 | fi 44 | 45 | # build for production 46 | echo "" 47 | echo "Build cbatmo for production" 48 | yarn run build 49 | 50 | echo "" 51 | echo "Start packaging version: ${VERSION}" 52 | 53 | # Move in the parent directory 54 | cd .. 55 | 56 | # Generate the archive 57 | tar czf ${TAR_NAME} \ 58 | --exclude="${DIRECTOY_NAME}/client" \ 59 | --exclude="${DIRECTOY_NAME}/logs" \ 60 | --exclude="${DIRECTOY_NAME}/node_modules" \ 61 | --exclude="${DIRECTOY_NAME}/screenshots" \ 62 | --exclude="${DIRECTOY_NAME}/.git" \ 63 | ${DIRECTOY_NAME} 64 | 65 | echo "Generated archive with name $TAR_NAME" 66 | echo "" 67 | 68 | while true; do 69 | read -p "Do you want to upload this archive into your Raspberry Pi? " yn 70 | case $yn in 71 | [Yy]* ) break;; 72 | [Nn]* ) exit 0;; 73 | * ) echo "Please answer yes or no";; 74 | esac 75 | done 76 | 77 | while true; do 78 | read -p "Please specify your Raspberry Pi IP address: " RPI_IP 79 | case $RPI_IP in 80 | [1]* ) break;; 81 | * ) echo "Please specify a valid IP, e.g. 10.0.0.143";; 82 | esac 83 | done 84 | 85 | read -p "Please specify your Raspberry Pi username: " RPI_USER 86 | 87 | echo "Upload $TAR_NAME on your Raspberry Pi ${RPI_IP}" 88 | sftp $RPI_USER@$RPI_IP << SFTP_COMMANDS 89 | put $TAR_NAME 90 | quit 91 | SFTP_COMMANDS 92 | 93 | echo "" 94 | while true; do 95 | read -p "Do you want to install cbatmo into your Raspberry Pi? " yn 96 | case $yn in 97 | [Yy]* ) break;; 98 | [Nn]* ) exit 0;; 99 | * ) echo "Please answer yes or no";; 100 | esac 101 | done 102 | 103 | echo "Install CBatmo on your Raspberry Pi ${RPI_IP}" 104 | ssh $RPI_USER@$RPI_IP << SSH_COMMANDS 105 | rm -Rf cbatmo 106 | tar xzf $TAR_NAME 107 | cd cbatmo 108 | yarn install --production 109 | sudo cp rpi/cbatmo.service /etc/systemd/system/ 110 | sudo systemctl enable cbatmo.service 111 | sudo systemctl restart cbatmo.service 112 | sleep 2 113 | systemctl status cbatmo.service 114 | exit 115 | SSH_COMMANDS 116 | 117 | exit 0; 118 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoWind.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Colors } from "@blueprintjs/core"; 3 | import { withTranslation, WithTranslation } from 'react-i18next'; 4 | import * as i18next from 'i18next'; 5 | import ModuleLayout from "../layouts/ModuleLayout"; 6 | import { INetatmoNAModule2 } from "../models/NetatmoNAModule2"; 7 | import * as netatmoActions from "../store/netatmo/actions"; 8 | import {ConnectedReduxProps} from "../store"; 9 | import {Orientation} from "../store/application/types"; 10 | import {Timelapse} from "../types/netatmo"; 11 | 12 | // Separate state props + dispatch props to their own interfaces. 13 | interface IPropsFromState { 14 | module_data: INetatmoNAModule2|undefined 15 | device_id: string|undefined 16 | unit: string 17 | selected_timelapse: Timelapse 18 | wind_ratio: number 19 | orientation: Orientation 20 | } 21 | 22 | // We can use `typeof` here to map our dispatch types to the props, like so. 23 | interface IPropsFromDispatch extends WithTranslation { 24 | [key: string]: any 25 | fetchMeasure: typeof netatmoActions.fetchMeasure 26 | t: i18next.TFunction 27 | } 28 | 29 | // Combine both state + dispatch props - as well as any props we want to pass - in a union type. 30 | type AllProps = IPropsFromState & IPropsFromDispatch & ConnectedReduxProps; 31 | 32 | /** Rain module */ 33 | const NetatmoModuleWind: React.FunctionComponent = (props) => { 34 | return ( 35 | 43 |
44 |
45 |
props.fetchMeasure(props.device_id as string, props.module_data?.id as string, ['windStrength'], props.selected_timelapse)}> 46 |
{props.t('netatmo.wind_strength')}
47 | {props.module_data?.data?.wind_strength}{props.unit} 48 |
49 | 50 | { 51 | props.orientation === 'portrait' && ( 52 |
53 | 54 |
55 | ) 56 | } 57 | 58 |
59 |
{props.t('netatmo.wind_max_day')}
60 | {props.module_data?.data?.max_wind_str}{props.unit} 61 |
62 |
63 | { 64 | props.orientation === 'landscape' && ( 65 |
66 |
67 | 68 |
69 |
70 | ) 71 | } 72 |
73 |
74 | ) 75 | }; 76 | 77 | export default withTranslation('common')(NetatmoModuleWind) 78 | -------------------------------------------------------------------------------- /src/components/ModuleDateTime.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Colors } from '@blueprintjs/core'; 3 | import cx from 'classnames'; 4 | import removeAccents from 'remove-accents'; 5 | import { withTranslation, WithTranslation } from 'react-i18next'; 6 | import * as i18next from 'i18next'; 7 | import { momentWithLocale } from '../utils/tools'; 8 | import {ConnectedReduxProps} from "../store"; 9 | import ModuleLayout from "../layouts/ModuleLayout"; 10 | import {Orientation} from "../store/application/types"; 11 | 12 | // Separate state props + dispatch props to their own interfaces. 13 | interface IPropsFromState { 14 | locale: string 15 | sunset_time: number 16 | sunrise_time: number 17 | orientation: Orientation 18 | } 19 | 20 | interface IpropsFromDispatch extends WithTranslation { 21 | [key: string]: any 22 | t: i18next.TFunction 23 | } 24 | 25 | type AllProps = IPropsFromState & IpropsFromDispatch & ConnectedReduxProps; 26 | 27 | interface IState { 28 | hour: string 29 | minutes: string 30 | seconds: string 31 | date: string 32 | } 33 | 34 | class ModuleDateTime extends React.Component { 35 | private interval: number | undefined; 36 | 37 | public state: IState = { 38 | hour: '00', 39 | minutes: '00', 40 | seconds: '00', 41 | date: '' 42 | }; 43 | 44 | public componentDidMount(): void { 45 | this.clock(); 46 | 47 | this.interval = window.setInterval(() => { 48 | this.clock() 49 | }, 1000); 50 | } 51 | 52 | public componentWillUnmount(): void { 53 | clearInterval(this.interval); 54 | } 55 | 56 | private clock = (): void => { 57 | let moment = momentWithLocale(this.props.locale); 58 | const date = moment(); 59 | 60 | this.setState({ 61 | hour: date.format('HH'), 62 | minutes: date.format('mm'), 63 | seconds: date.format('ss'), 64 | date: date.format('dddd DD MMMM') 65 | }); 66 | }; 67 | 68 | render() { 69 | let moment = momentWithLocale(this.props.locale); 70 | 71 | return ( 72 | 73 |
74 | { 75 | this.props.orientation === 'landscape' ? ( 76 |
{ this.state.hour }:{ this.state.minutes }{this.state.seconds}
77 | ) : null 78 | } 79 |
{ removeAccents(this.state.date) }
80 |
81 |
82 |
{this.props.t('forecast.sunrise')}
83 | {moment.unix(this.props.sunrise_time ? this.props.sunrise_time : 0).format('HH:mm')} 84 |
85 |
86 |
{this.props.t('forecast.sunset')}
87 | {moment.unix(this.props.sunset_time ? this.props.sunset_time : 0).format('HH:mm')} 88 |
89 |
90 |
91 |
92 | ) 93 | } 94 | } 95 | 96 | export default withTranslation('common')(ModuleDateTime); 97 | -------------------------------------------------------------------------------- /src/models/NetatmoNAModule3.ts: -------------------------------------------------------------------------------- 1 | import {INetatmoUserInformation} from "./NetatmoUserInformation"; 2 | import {BatteryLevel, DataTypes, RadioLevel} from "../types/netatmo"; 3 | 4 | export interface INetatmoNAModule3 { 5 | id: string 6 | type: string 7 | module_name: string 8 | data_type: DataTypes[] 9 | reachable: boolean 10 | last_seen: number 11 | rf_status: number 12 | radio: RadioLevel 13 | battery_vp: number 14 | battery: BatteryLevel 15 | battery_percent: number 16 | data: IData|undefined 17 | } 18 | 19 | export interface IData { 20 | rain: number 21 | sum_rain_24: number 22 | sum_rain_1: number 23 | } 24 | 25 | /** Rain module model */ 26 | class NetatmoNAModule3 implements INetatmoNAModule3 { 27 | id: string; 28 | type: string; 29 | module_name: string; 30 | data_type: DataTypes[]; 31 | reachable: boolean; 32 | last_seen: number; 33 | rf_status: number; 34 | radio: RadioLevel; 35 | battery_vp: number; 36 | battery: BatteryLevel; 37 | battery_percent: number; 38 | data: IData|undefined; 39 | 40 | constructor(data: any, userInfo: INetatmoUserInformation) { 41 | this.id = data._id; 42 | this.type = data.type; 43 | this.module_name = data.module_name || 'Rain'; // Default name in case of no module name given 44 | this.data_type = data.data_type; 45 | this.reachable = data.reachable; 46 | this.last_seen = data.last_seen; 47 | this.rf_status = data.rf_status; 48 | this.radio = data.radio; 49 | this.battery_vp = data.battery_vp; 50 | this.battery = data.battery; 51 | this.battery_percent = data.battery_percent; 52 | 53 | // Set radio status 54 | switch (true) { 55 | case (data.rf_status >= 90): 56 | this.radio = '1'; 57 | break; 58 | case (data.rf_status < 90 && data.rf_status > 80): 59 | this.radio = '2'; 60 | break; 61 | case (data.rf_status <= 80 && data.rf_status > 70): 62 | this.radio = '3'; 63 | break; 64 | case (data.rf_status <= 70 && data.rf_status > 60): 65 | this.radio = '4'; 66 | break; 67 | default: 68 | this.radio = '5'; 69 | break; 70 | } 71 | 72 | this.battery_vp = data.battery_vp; 73 | 74 | // Set battery status 75 | switch (true) { 76 | case (data.battery_vp >= 6000): 77 | this.battery = 'max'; 78 | break; 79 | case (data.battery_vp < 6000 && data.battery_vp >= 5500): 80 | this.battery = 'full'; 81 | break; 82 | case (data.battery_vp < 5500 && data.battery_vp >= 5000): 83 | this.battery = 'high'; 84 | break; 85 | case (data.battery_vp < 5000 && data.battery_vp >= 4500): 86 | this.battery = 'medium'; 87 | break; 88 | case (data.battery_vp < 4500 && data.battery_vp >= 4000): 89 | this.battery = 'low'; 90 | break; 91 | case (data.battery_vp < 4000): 92 | this.battery = 'very-low'; 93 | break; 94 | } 95 | 96 | this.battery_percent = data.battery_percent; 97 | 98 | if (this.reachable) { 99 | this.data = { 100 | rain: Number((data.dashboard_data.Rain / userInfo.rain_ratio).toFixed(userInfo.unit === 'si' ? 1 : 3)), 101 | sum_rain_24: Number((data.dashboard_data.sum_rain_24 / userInfo.rain_ratio).toFixed(userInfo.unit === 'si' ? 1 : 3)), // Last 24 hours 102 | sum_rain_1: Number((data.dashboard_data.sum_rain_1 / userInfo.rain_ratio).toFixed(userInfo.unit === 'si' ? 1 : 3)) // Last hour 103 | } 104 | } 105 | } 106 | } 107 | 108 | export default NetatmoNAModule3 109 | -------------------------------------------------------------------------------- /src/models/NetatmoNAModule1.ts: -------------------------------------------------------------------------------- 1 | import {INetatmoUserInformation} from "./NetatmoUserInformation"; 2 | import {BatteryLevel, DataTypes, RadioLevel} from "../types/netatmo"; 3 | 4 | export interface INetatmoNAModule1 { 5 | id: string 6 | type: string 7 | module_name: string 8 | data_type: DataTypes[] 9 | reachable: boolean 10 | last_seen: number 11 | rf_status: number 12 | radio: RadioLevel 13 | battery_vp: number 14 | battery: BatteryLevel 15 | battery_percent: number 16 | data: IData|undefined 17 | } 18 | 19 | export interface IData { 20 | temperature: number 21 | humidity: number 22 | min_temp: number 23 | max_temp: number 24 | temp_trend: string 25 | } 26 | 27 | /** Outdoor module model */ 28 | class NetatmoNAModule1 implements INetatmoNAModule1{ 29 | id: string; 30 | type: string; 31 | module_name: string; 32 | data_type: DataTypes[]; 33 | reachable: boolean; 34 | last_seen: number; 35 | rf_status: number; 36 | radio: RadioLevel; 37 | battery_vp: number; 38 | battery: BatteryLevel; 39 | battery_percent: number; 40 | data: IData|undefined; 41 | 42 | constructor(data: any, userInfo: INetatmoUserInformation) { 43 | this.id = data._id; 44 | this.type = data.type; 45 | this.module_name = data.module_name || 'Outdoor'; // Default name in case of no module name given 46 | this.data_type = data.data_type; 47 | this.reachable = data.reachable; 48 | this.last_seen = data.last_seen; 49 | this.rf_status = data.rf_status; 50 | this.radio = data.radio; 51 | this.battery_vp = data.battery_vp; 52 | this.battery = data.battery; 53 | this.battery_percent = data.battery_percent; 54 | 55 | // Set radio status 56 | switch (true) { 57 | case (data.rf_status >= 90): 58 | this.radio = '1'; 59 | break; 60 | case (data.rf_status < 90 && data.rf_status > 80): 61 | this.radio = '2'; 62 | break; 63 | case (data.rf_status <= 80 && data.rf_status > 70): 64 | this.radio = '3'; 65 | break; 66 | case (data.rf_status <= 70 && data.rf_status > 60): 67 | this.radio = '4'; 68 | break; 69 | default: 70 | this.radio = '5'; 71 | break; 72 | } 73 | 74 | this.battery_vp = data.battery_vp; 75 | 76 | // Set battery status 77 | switch (true) { 78 | case (data.battery_vp >= 6000): 79 | this.battery = 'max'; 80 | break; 81 | case (data.battery_vp < 6000 && data.battery_vp >= 5500): 82 | this.battery = 'full'; 83 | break; 84 | case (data.battery_vp < 5500 && data.battery_vp >= 5000): 85 | this.battery = 'high'; 86 | break; 87 | case (data.battery_vp < 5000 && data.battery_vp >= 4500): 88 | this.battery = 'medium'; 89 | break; 90 | case (data.battery_vp < 4500 && data.battery_vp >= 4000): 91 | this.battery = 'low'; 92 | break; 93 | case (data.battery_vp < 4000): 94 | this.battery = 'very-low'; 95 | break; 96 | } 97 | 98 | this.battery_percent = data.battery_percent; 99 | 100 | if (this.reachable) { 101 | this.data = { 102 | temperature: Math.round(eval(data.dashboard_data.Temperature + '*' + userInfo.temperature_ratio) * 10) / 10, 103 | humidity: data.dashboard_data.Humidity, 104 | min_temp: Math.round(eval(data.dashboard_data.min_temp + '*' + userInfo.temperature_ratio) * 10) / 10, 105 | max_temp: Math.round(eval(data.dashboard_data.max_temp + '*' + userInfo.temperature_ratio) * 10) / 10, 106 | temp_trend: data.dashboard_data.temp_trend 107 | } 108 | } 109 | } 110 | } 111 | 112 | export default NetatmoNAModule1 113 | -------------------------------------------------------------------------------- /src/models/NetatmoNAModule4.ts: -------------------------------------------------------------------------------- 1 | import {INetatmoUserInformation} from "./NetatmoUserInformation"; 2 | import {BatteryLevel, DataTypes, RadioLevel} from "../types/netatmo"; 3 | 4 | export interface INetatmoNAModule4 { 5 | id: string 6 | type: string 7 | module_name: string 8 | data_type: DataTypes[] 9 | reachable: boolean 10 | last_seen: number 11 | rf_status: number 12 | radio: RadioLevel 13 | battery_vp: number 14 | battery: BatteryLevel 15 | battery_percent: number 16 | data: IData|undefined 17 | } 18 | 19 | export interface IData { 20 | temperature: number 21 | co2: number 22 | humidity: number 23 | min_temp: number 24 | max_temp: number 25 | temp_trend: string 26 | } 27 | 28 | /** Indoor moduke model */ 29 | class NetatmoNAModule4 { 30 | id: string; 31 | type: string; 32 | module_name: string; 33 | data_type: DataTypes[]; 34 | reachable: boolean; 35 | last_seen: number; 36 | rf_status: number; 37 | radio: RadioLevel; 38 | battery_vp: number; 39 | battery: BatteryLevel; 40 | battery_percent: number; 41 | data: IData|undefined; 42 | 43 | constructor(data: any, userInfo: INetatmoUserInformation) { 44 | this.id = data._id; 45 | this.type = data.type; 46 | this.module_name = data.module_name || 'Indoor'; // Default name in case of no module name given 47 | this.data_type = data.data_type; 48 | this.reachable = data.reachable; 49 | this.last_seen = data.last_seen; 50 | this.rf_status = data.rf_status; 51 | this.radio = data.radio; 52 | this.battery_vp = data.battery_vp; 53 | this.battery = data.battery; 54 | this.battery_percent = data.battery_percent; 55 | 56 | // Set radio status 57 | switch (true) { 58 | case (data.rf_status >= 90): 59 | this.radio = '1'; 60 | break; 61 | case (data.rf_status < 90 && data.rf_status > 80): 62 | this.radio = '2'; 63 | break; 64 | case (data.rf_status <= 80 && data.rf_status > 70): 65 | this.radio = '3'; 66 | break; 67 | case (data.rf_status <= 70 && data.rf_status > 60): 68 | this.radio = '4'; 69 | break; 70 | default: 71 | this.radio = '5'; 72 | break; 73 | } 74 | 75 | this.battery_vp = data.battery_vp; 76 | 77 | // Set battery status 78 | switch (true) { 79 | case (data.battery_vp >= 6000): 80 | this.battery = 'max'; 81 | break; 82 | case (data.battery_vp < 6000 && data.battery_vp >= 5640): 83 | this.battery = 'full'; 84 | break; 85 | case (data.battery_vp < 5640 && data.battery_vp >= 5280): 86 | this.battery = 'high'; 87 | break; 88 | case (data.battery_vp < 5280 && data.battery_vp >= 4920): 89 | this.battery = 'medium'; 90 | break; 91 | case (data.battery_vp < 4920 && data.battery_vp >= 4560): 92 | this.battery = 'low'; 93 | break; 94 | case (data.battery_vp < 4560): 95 | this.battery = 'very-low'; 96 | break; 97 | } 98 | 99 | this.battery_percent = data.battery_percent; 100 | 101 | if (this.reachable) { 102 | this.data = { 103 | temperature: Math.round(eval(data.dashboard_data.Temperature + '*' + userInfo.temperature_ratio) * 10) / 10, 104 | co2: data.dashboard_data.CO2, 105 | humidity: data.dashboard_data.Humidity, 106 | min_temp: Math.round(eval(data.dashboard_data.min_temp + '*' + userInfo.temperature_ratio) * 10) / 10, 107 | max_temp: Math.round(eval(data.dashboard_data.max_temp + '*' + userInfo.temperature_ratio) * 10) / 10, 108 | temp_trend: data.dashboard_data.temp_trend 109 | } 110 | } 111 | } 112 | } 113 | 114 | export default NetatmoNAModule4 115 | -------------------------------------------------------------------------------- /src/models/NetatmoNAModule2.ts: -------------------------------------------------------------------------------- 1 | import {INetatmoUserInformation} from "./NetatmoUserInformation"; 2 | import {BatteryLevel, DataTypes, RadioLevel} from "../types/netatmo"; 3 | 4 | export interface INetatmoNAModule2 { 5 | id: string 6 | type: string 7 | module_name: string 8 | data_type: DataTypes[] 9 | reachable: boolean 10 | last_seen: number 11 | rf_status: number 12 | radio: RadioLevel 13 | battery_vp: number 14 | battery: BatteryLevel 15 | battery_percent: number 16 | data: IData|undefined 17 | } 18 | 19 | export interface IData { 20 | wind_strength: number 21 | wind_angle: number 22 | gust_strength: number 23 | gust_angle: number 24 | max_wind_str: number 25 | max_wind_angle: number 26 | } 27 | 28 | /** Wind module model */ 29 | class NetatmoNAModule2 implements INetatmoNAModule2 { 30 | id: string; 31 | type: string; 32 | module_name: string; 33 | data_type: DataTypes[]; 34 | reachable: boolean; 35 | last_seen: number; 36 | rf_status: number; 37 | radio: RadioLevel; 38 | battery_vp: number; 39 | battery: BatteryLevel; 40 | battery_percent: number; 41 | data: IData|undefined; 42 | 43 | constructor(data: any, userInfo: INetatmoUserInformation) { 44 | this.id = data._id; 45 | this.type = data.type; 46 | this.module_name = data.module_name || 'Wind'; // Default name in case of no module name given 47 | this.data_type = data.data_type; 48 | this.reachable = data.reachable; 49 | this.last_seen = data.last_seen; 50 | this.rf_status = data.rf_status; 51 | this.radio = data.radio; 52 | this.battery_vp = data.battery_vp; 53 | this.battery = data.battery; 54 | this.battery_percent = data.battery_percent; 55 | 56 | // Set radio status 57 | switch (true) { 58 | case (data.rf_status >= 90): 59 | this.radio = '1'; 60 | break; 61 | case (data.rf_status < 90 && data.rf_status > 80): 62 | this.radio = '2'; 63 | break; 64 | case (data.rf_status <= 80 && data.rf_status > 70): 65 | this.radio = '3'; 66 | break; 67 | case (data.rf_status <= 70 && data.rf_status > 60): 68 | this.radio = '4'; 69 | break; 70 | default: 71 | this.radio = '5'; 72 | break; 73 | } 74 | 75 | this.battery_vp = data.battery_vp; 76 | 77 | // Set battery status 78 | switch (true) { 79 | case (data.battery_vp >= 6000): 80 | this.battery = 'max'; 81 | break; 82 | case (data.battery_vp < 6000 && data.battery_vp >= 5590): 83 | this.battery = 'full'; 84 | break; 85 | case (data.battery_vp < 5590 && data.battery_vp >= 5180): 86 | this.battery = 'high'; 87 | break; 88 | case (data.battery_vp < 5180 && data.battery_vp >= 4770): 89 | this.battery = 'medium'; 90 | break; 91 | case (data.battery_vp < 4770 && data.battery_vp >= 4360): 92 | this.battery = 'low'; 93 | break; 94 | case (data.battery_vp < 4360): 95 | this.battery = 'very-low'; 96 | break; 97 | } 98 | 99 | this.battery_percent = data.battery_percent; 100 | 101 | if (this.reachable) { 102 | this.data = { 103 | wind_strength: Math.round(data.dashboard_data.WindStrength * userInfo.wind_ratio), 104 | wind_angle: data.dashboard_data.WindAngle, 105 | gust_strength: Math.round(data.dashboard_data.GustStrength * userInfo.wind_ratio), 106 | gust_angle: data.dashboard_data.GustAngle, 107 | max_wind_str: Math.round(data.dashboard_data.max_wind_str * userInfo.wind_ratio), 108 | max_wind_angle: data.dashboard_data.max_wind_angle 109 | } 110 | } 111 | } 112 | } 113 | 114 | export default NetatmoNAModule2 115 | -------------------------------------------------------------------------------- /src/models/NetatmoUserInformation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * unit : 0 -> metric system, 1 -> imperial system 3 | * windunit: 0 -> kph, 1 -> mph, 2 -> ms, 3 -> beaufort, 4 -> knot 4 | * pressureunit: 0 -> mbar, 1 -> inHg, 2 -> mmHg 5 | * lang: user locale 6 | * reg_locale: user regional preferences (used for displaying date) 7 | * feel_like: algorithm used to compute feel like temperature, 0 -> humidex, 1 -> heat-index 8 | */ 9 | 10 | export interface INetatmoUserInformation { 11 | mail: string 12 | lang: string 13 | locale: string 14 | pressure_unit: string 15 | unit: string 16 | temperature_unit: string 17 | distance_unit: string 18 | windunit: string 19 | temperature_ratio: string 20 | wind_ratio: number 21 | pressure_ratio: number 22 | rain_ratio: number 23 | } 24 | 25 | 26 | class NetatmoUserInformation implements INetatmoUserInformation{ 27 | mail: string 28 | lang: string 29 | locale: string 30 | pressure_unit: string 31 | unit: string 32 | temperature_unit: string 33 | distance_unit: string 34 | windunit: string 35 | temperature_ratio: string 36 | wind_ratio: number 37 | pressure_ratio: number 38 | rain_ratio: number 39 | 40 | constructor(data: any) { 41 | this.mail = data.mail; 42 | this.locale = data.administrative.reg_locale; 43 | 44 | if (data.administrative.lang.includes('fr')) { 45 | this.lang = 'fr'; 46 | } else if (data.administrative.lang.includes('de')) { 47 | this.lang = 'de'; 48 | } else { 49 | this.lang = 'en'; 50 | } 51 | 52 | switch (data.administrative.pressureunit) { 53 | case 0: 54 | this.pressure_unit = 'mbar'; 55 | this.pressure_ratio = 1; 56 | break; 57 | case 1: 58 | this.pressure_unit = 'inHg'; 59 | this.pressure_ratio = 0.02953; 60 | break; 61 | case 2: 62 | this.pressure_unit = 'mmHg'; 63 | this.pressure_ratio = 0.750062; 64 | break; 65 | default: 66 | this.pressure_unit = 'mbar'; 67 | this.pressure_ratio = 1; 68 | break; 69 | } 70 | 71 | switch (data.administrative.unit) { 72 | case 0: 73 | this.unit = 'si'; 74 | this.temperature_unit = 'C'; 75 | this.distance_unit = 'mm'; 76 | this.temperature_ratio = '1'; 77 | this.rain_ratio = 1; 78 | break; 79 | case 1: 80 | this.unit = 'us'; 81 | this.temperature_unit = 'F'; 82 | this.distance_unit = 'in'; 83 | this.temperature_ratio = '9/5 + 32'; 84 | this.rain_ratio = 25.4; 85 | break; 86 | default: 87 | this.unit = 'si'; 88 | this.temperature_unit = 'C'; 89 | this.distance_unit = 'mm'; 90 | this.temperature_ratio = '1'; 91 | this.rain_ratio = 1; 92 | break; 93 | } 94 | 95 | switch (data.administrative.windunit) { 96 | case 0: 97 | this.windunit = 'km/h'; 98 | this.wind_ratio = 1; 99 | break; 100 | case 1: 101 | this.windunit = 'mp/h'; 102 | this.wind_ratio = 0.621371; 103 | break; 104 | case 2: 105 | this.windunit = 'ms'; 106 | this.wind_ratio = 0.2777778; 107 | break; 108 | case 3: 109 | this.windunit = 'beaufort'; 110 | this.wind_ratio = 1; // TODO 111 | // Currently not supported : https://www.meteosuisse.admin.ch/content/dam/meteoswiss/fr/Wetter/Prognosen/Wetterbegriffe/Doc/wetter-tabelle-force-des-vents.pdf 112 | break; 113 | case 4: 114 | this.windunit = 'kts'; 115 | this.wind_ratio = 0.5399568; 116 | break; 117 | default: 118 | this.windunit = 'km/h'; 119 | this.wind_ratio = 1; 120 | break; 121 | } 122 | 123 | 124 | console.debug(this) 125 | } 126 | 127 | public getTemperatureRatio = (): string => { 128 | return this.temperature_ratio 129 | } 130 | 131 | public getWindRatio = (): number => { 132 | return this.wind_ratio 133 | } 134 | 135 | public getPressureRatio = (): number => { 136 | return this.pressure_ratio 137 | } 138 | 139 | public getRainRatio = (): number => { 140 | return this.pressure_ratio 141 | } 142 | } 143 | 144 | export default NetatmoUserInformation 145 | -------------------------------------------------------------------------------- /src/layouts/DashboardLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import * as openweatherActions from "../store/openweather/actions"; 3 | import * as netatmoActions from "../store/netatmo/actions"; 4 | import * as applicationActions from "../store/application/actions"; 5 | import { ConnectedReduxProps } from "../store"; 6 | import { INetatmoNAMain } from "../models/NetatmoNAMain"; 7 | import {Orientation} from "../store/application/types"; 8 | import {DataTypes, Timelapse} from "../types/netatmo"; 9 | 10 | // Separate state props + dispatch props to their own interfaces. 11 | interface IPropsFromState { 12 | children: ReactNode 13 | station_data: INetatmoNAMain|undefined 14 | selected_module: string 15 | selected_types: DataTypes[] 16 | selected_station_type: DataTypes 17 | selected_outdoor_type: DataTypes 18 | selected_indoor_type: DataTypes 19 | selected_indoor_second_type: DataTypes 20 | selected_indoor_third_type: DataTypes 21 | selected_timelapse: Timelapse 22 | mobile?: string 23 | } 24 | 25 | // We can use `typeof` here to map our dispatch types to the props, like so. 26 | interface IPropsFromDispatch { 27 | [key: string]: any 28 | fetchOpenWeather: typeof openweatherActions.fetchOpenWeather 29 | fetchStationData: typeof netatmoActions.fetchStationData 30 | fetchMeasure: typeof netatmoActions.fetchMeasure 31 | fetchMeasures: typeof netatmoActions.fetchMeasures 32 | fetchRainMeasure: typeof netatmoActions.fetchRainMeasure 33 | setOrientation: typeof applicationActions.setOrientation 34 | } 35 | 36 | // Combine both state + dispatch props - as well as any props we want to pass - in a union type. 37 | type AllProps = IPropsFromState & IPropsFromDispatch & ConnectedReduxProps; 38 | 39 | const INTERVAL_IN_MINUTES = 1, REFRESH_TIME = INTERVAL_IN_MINUTES * 60 * 1000; 40 | 41 | class DashboardLayout extends React.Component { 42 | private interval: number | undefined; 43 | 44 | public componentDidMount(): void { 45 | // Get current orientation 46 | const angle = window.orientation; // Deprecated feature but only this work on iOS Safari 47 | this.props.setOrientation(this._getOrientation(angle as number)); 48 | 49 | // Listen mobile orientation 50 | window.addEventListener('orientationchange', (e: Event) => { 51 | const angle = window.orientation; 52 | this.props.setOrientation(this._getOrientation(angle as number)); 53 | }, true) 54 | 55 | this.props.fetchOpenWeather(); 56 | 57 | // Fetch on app load all modules measures 58 | this._fetchNetatmoModulesMeasures(); 59 | 60 | this.interval = window.setInterval(() => { 61 | this.props.fetchOpenWeather(); 62 | this.props.fetchStationData(); 63 | this._fetchNetatmoModulesMeasures(); 64 | }, REFRESH_TIME); 65 | } 66 | 67 | public componentWillUnmount(): void { 68 | clearInterval(this.interval); 69 | } 70 | 71 | private _getOrientation = (angle: number): Orientation => { 72 | if (angle === 0) { 73 | return 'portrait'; 74 | } else { 75 | return 'landscape'; 76 | } 77 | } 78 | 79 | private _fetchNetatmoModulesMeasures = (): void => { 80 | if (!!this.props.mobile) { 81 | this.props.fetchMeasures(this.props.station_data?.id as string, this.props.station_data?.id as string, this.props.station_data?.data_type as DataTypes[], '1d', 'station'); 82 | 83 | if (this.props.station_data?.available_modules.OUTDOOR) { 84 | this.props.fetchMeasures(this.props.station_data?.id as string, this.props.station_data.modules.OUTDOOR?.id as string, this.props.station_data.modules.OUTDOOR?.data_type as DataTypes[], '1d', 'outdoor'); 85 | } 86 | if (this.props.station_data?.available_modules.INDOOR) { 87 | this.props.fetchMeasures(this.props.station_data?.id as string, this.props.station_data.modules.INDOOR?.id as string, this.props.station_data.modules.INDOOR?.data_type as DataTypes[], '1d', 'indoor'); 88 | } 89 | if (this.props.station_data?.available_modules.INDOOR_SECOND) { 90 | this.props.fetchMeasures(this.props.station_data?.id as string, this.props.station_data.modules.INDOOR_SECOND?.id as string, this.props.station_data.modules.INDOOR_SECOND?.data_type as DataTypes[], '1d', 'indoor_second'); 91 | } 92 | if (this.props.station_data?.available_modules.INDOOR_THIRD) { 93 | this.props.fetchMeasures(this.props.station_data?.id as string, this.props.station_data.modules.INDOOR_THIRD?.id as string, this.props.station_data.modules.INDOOR_THIRD?.data_type as DataTypes[], '1d', 'indoor_third'); 94 | } 95 | } 96 | 97 | if (this.props.station_data?.available_modules.RAIN) { 98 | this.props.fetchRainMeasure(this.props.station_data?.id as string, this.props.station_data?.modules.RAIN?.id as string); 99 | } 100 | 101 | this.props.fetchMeasure(this.props.station_data?.id as string, this.props.selected_module as string, this.props.selected_types, this.props.selected_timelapse); 102 | }; 103 | 104 | public render() { 105 | return ( 106 | <> 107 | {this.props.children} 108 | 109 | ) 110 | } 111 | } 112 | 113 | export default DashboardLayout; 114 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoOutdoor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, ButtonGroup, Colors} from "@blueprintjs/core"; 3 | import { withTranslation, WithTranslation } from 'react-i18next'; 4 | import * as i18next from 'i18next'; 5 | import ModuleLayout from "../layouts/ModuleLayout"; 6 | import { INetatmoNAModule1 } from "../models/NetatmoNAModule1"; 7 | import * as netatmoActions from "../store/netatmo/actions"; 8 | import {ConnectedReduxProps} from "../store"; 9 | import {Orientation} from "../store/application/types"; 10 | import {Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis} from "recharts"; 11 | import {colorChooser} from "../utils/tools"; 12 | import {DataTypes, Timelapse} from "../types/netatmo"; 13 | 14 | // Separate state props + dispatch props to their own interfaces. 15 | interface IPropsFromState { 16 | module_data: INetatmoNAModule1|undefined 17 | device_id: string|undefined 18 | selected_timelapse: Timelapse 19 | temperature_ratio: string 20 | temperature_unit: string 21 | orientation: Orientation 22 | selected_type: DataTypes 23 | measure_data: [] 24 | } 25 | 26 | // We can use `typeof` here to map our dispatch types to the props, like so. 27 | interface IPropsFromDispatch extends WithTranslation { 28 | [key: string]: any 29 | fetchMeasure: typeof netatmoActions.fetchMeasure 30 | onChangeSelectedType: typeof netatmoActions.onChangeSelectedType 31 | t: i18next.TFunction 32 | } 33 | 34 | // Combine both state + dispatch props - as well as any props we want to pass - in a union type. 35 | type AllProps = IPropsFromState & IPropsFromDispatch & ConnectedReduxProps; 36 | 37 | /** Outdoor module */ 38 | const NetatmoModuleOutdoor: React.FunctionComponent = (props) => { 39 | const _onClick = (type: string) => { 40 | if (props.orientation !== 'portrait') { 41 | props.fetchMeasure(props.device_id as string, props.module_data?.id as string, [type], props.selected_timelapse); 42 | } 43 | } 44 | 45 | return ( 46 | 55 |
56 |
57 |
_onClick('Temperature')}> 58 |
{props.t('netatmo.temperature')}
59 | {props.module_data?.data?.temperature}°{props.temperature_unit} 60 |
61 |
_onClick('Humidity')} style={{textAlign: 'right'}}> 62 |
{props.t('netatmo.humidity')}
63 | {props.module_data?.data?.humidity}% 64 |
65 |
66 | { 67 | props.orientation === 'portrait' && ( 68 | <> 69 | 70 | { 71 | props.module_data?.data_type.map((type, index) => 72 | 77 | ) 78 | } 79 | 80 | 81 | 86 | 87 | 88 | 89 | 96 | 97 | 98 | 99 | ) 100 | } 101 |
102 |
103 | ) 104 | }; 105 | 106 | export default withTranslation('common')(NetatmoModuleOutdoor) 107 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const WebpackShellPluginNext = require('webpack-shell-plugin-next'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const WebpackPwaManifest = require('webpack-pwa-manifest') 7 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 8 | require('dotenv').config(); 9 | 10 | process.env.APP_ENV === 'dev' ? process.env.NODE_ENV = 'development' : process.env.NODE_ENV = 'production'; 11 | 12 | console.log('Webpack run as ' + process.env.NODE_ENV); 13 | 14 | const plugins = [ 15 | new MiniCssExtractPlugin({ 16 | filename: 'css/[name]' + (process.env.NODE_ENV !== 'development' ? '.[hash]' : '') + '.css', 17 | }), 18 | 19 | new HtmlWebpackPlugin({ 20 | template: '!!raw-loader!./src/index.ejs', 21 | filename: path.resolve(__dirname, 'views/index.ejs'), 22 | inject: 'body' 23 | }), 24 | // Ignore Moment Locales 25 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 26 | new WebpackPwaManifest({ 27 | name: 'CBatmo', 28 | short_name: 'CBatmo', 29 | description: 'A Netatmo Weather Station Web-APP for Raspberry Pi and official Raspberry touchscreen', 30 | background_color: '#1E1E1E', 31 | crossorigin: null, 32 | display: 'standalone', 33 | orientation: 'landscape' 34 | }), 35 | // Fix for blueprintjs Uncaught ReferenceError: process is not defined 36 | new webpack.DefinePlugin({ 37 | "process.env": "{}", 38 | global: {} 39 | }) 40 | ]; 41 | 42 | if (process.env.NODE_ENV === 'development') { 43 | plugins.push(new WebpackShellPluginNext({ 44 | onBuildEnd: { 45 | scripts: ['node server.js'], 46 | blocking: false, 47 | parallel: true 48 | } 49 | })); 50 | } 51 | 52 | if (process.env.NODE_ENV === 'analyse') { 53 | plugins.push(new BundleAnalyzerPlugin()) 54 | } 55 | 56 | module.exports = { 57 | mode: process.env.NODE_ENV, 58 | entry: { 59 | 'bundle': './src/index.tsx', 60 | }, 61 | resolve: { 62 | extensions: ['.js', '.ts', '.tsx'] 63 | }, 64 | module: { 65 | rules: [ 66 | { 67 | test: /\.(otf|eot|ttf|woff2?)$/, 68 | use: { 69 | loader: 'url-loader', 70 | options: { 71 | limit: 10000, 72 | outputPath: 'fonts/', 73 | publicPath: '/fonts' 74 | } 75 | } 76 | }, 77 | { 78 | test: /\.(css|sass|scss)$/, 79 | use: [ 80 | MiniCssExtractPlugin.loader, 81 | { 82 | loader: 'css-loader', // translates CSS into CommonJS 83 | options: { 84 | sourceMap: process.env.NODE_ENV === 'development' 85 | }, 86 | }, 87 | { 88 | loader: 'sass-loader', // compiles Sass to CSS 89 | options: { 90 | sourceMap: process.env.NODE_ENV === 'development' 91 | }, 92 | }, 93 | ] 94 | }, 95 | { 96 | test: /\.(png|jpe?g|gif|svg)$/, 97 | use: [ 98 | { 99 | loader: 'file-loader', 100 | options: { 101 | limit: 10000, 102 | outputPath: 'img/', 103 | publicPath: '/img' 104 | } 105 | }, 106 | { 107 | loader: 'image-webpack-loader', 108 | options: { 109 | optipng: { 110 | progressive: true, 111 | optimizationLevel: 7, 112 | interlaced: false, 113 | }, 114 | mozjpeg: { 115 | quality: 65 116 | }, 117 | pngquant: { 118 | quality: [0.65, 0.90], 119 | speed: 4 120 | }, 121 | svgo: { 122 | plugins: [ 123 | { 124 | removeViewBox: false 125 | }, 126 | { 127 | removeEmptyAttrs: false 128 | } 129 | ] 130 | } 131 | } 132 | } 133 | ] 134 | }, 135 | { test: /\.(t|j)sx?$/, use: { loader: 'awesome-typescript-loader' }, exclude: /node_modules/ }, 136 | { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }, 137 | { test: /\.html$/, loader: 'html-loader' } 138 | ] 139 | }, 140 | plugins, 141 | 142 | devtool: process.env.NODE_ENV === 'development' && 'source-map', 143 | 144 | output: { 145 | publicPath: '/', 146 | path: path.resolve(__dirname, 'public'), 147 | filename: '[name]' + (process.env.NODE_ENV !== "development" ? '.[hash]' : '') + '.js' 148 | } 149 | }; 150 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoGraph.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alignment, Button, ButtonGroup, Colors } from "@blueprintjs/core"; 3 | import { AreaChart, Area, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from 'recharts'; 4 | import removeAccents from 'remove-accents'; 5 | import { withTranslation, WithTranslation } from 'react-i18next'; 6 | import * as i18next from 'i18next'; 7 | import ModuleLayout from "../layouts/ModuleLayout"; 8 | import {DataTypes, Timelapse} from "../types/netatmo"; 9 | import {colorChooser} from "../utils/tools"; 10 | import {INetatmoNAMain} from "../models/NetatmoNAMain"; 11 | import * as netatmoActions from "../store/netatmo/actions"; 12 | import {ConnectedReduxProps} from "../store"; 13 | import { Orientation } from "../store/application/types"; 14 | 15 | // Separate state props + dispatch props to their own interfaces. 16 | interface IPropsFromState { 17 | phone?: string 18 | mobile?: string 19 | orientation: Orientation 20 | measure_data: [] 21 | selected_types: DataTypes[] 22 | selected_module: string 23 | selected_timelapse: Timelapse 24 | station_data: INetatmoNAMain|undefined 25 | } 26 | 27 | // We can use `typeof` here to map our dispatch types to the props, like so. 28 | interface IPropsFromDispatch extends WithTranslation { 29 | [key: string]: any 30 | fetchMeasure: typeof netatmoActions.fetchMeasure 31 | t: i18next.TFunction 32 | } 33 | 34 | // Combine both state + dispatch props - as well as any props we want to pass - in a union type. 35 | type AllProps = IPropsFromState & IPropsFromDispatch & ConnectedReduxProps; 36 | 37 | /** Rain module */ 38 | class NetatmoModuleGraph extends React.Component { 39 | 40 | private findModuleName = (module_id: string) => { 41 | // @ts-ignore 42 | const { modules } = this.props.station_data; 43 | 44 | if (module_id === this.props.station_data?.id) { 45 | return this.props.station_data.module_name; 46 | } else if (module_id === modules.INDOOR?.id) { 47 | return modules.INDOOR.module_name; 48 | } else if (module_id === modules.INDOOR_SECOND?.id) { 49 | return modules.INDOOR_SECOND.module_name; 50 | } else if (module_id === modules.INDOOR_THIRD?.id) { 51 | return modules.INDOOR_THIRD.module_name; 52 | } else if (module_id === modules.OUTDOOR?.id) { 53 | return modules.OUTDOOR.module_name; 54 | } else if (module_id === modules.RAIN?.id) { 55 | return modules.RAIN.module_name; 56 | } else if (module_id === modules.WIND?.id) { 57 | return modules.WIND.module_name; 58 | } else { 59 | return ''; 60 | } 61 | }; 62 | 63 | private handleOnclick = (timelapse: Timelapse): void => { 64 | this.props.fetchMeasure(this.props.station_data?.id as string, this.props.selected_module, this.props.selected_types, timelapse); 65 | }; 66 | 67 | private _setGraphHeight = (phone: boolean, orientation: Orientation, number_of_additional_modules: number): number => { 68 | if (phone && orientation === 'portrait') { 69 | return 144 70 | } else if (phone && orientation === 'landscape') { 71 | if (number_of_additional_modules === 0) { 72 | return 94 * 2.6 73 | } else { 74 | return 94 75 | } 76 | } else { 77 | if (number_of_additional_modules === 0) { 78 | return 122 * 2.6 79 | } else { 80 | return 122 81 | } 82 | } 83 | } 84 | 85 | public render() { 86 | return ( 87 | 93 |
94 | 95 | 99 | 103 | 107 | 108 | 109 | 115 | 116 | 117 | 118 | 125 | 126 | 127 |
128 |
129 | {removeAccents(this.findModuleName(this.props.selected_module))} - {this.props.t('netatmo.' + this.props.selected_types[0].toLowerCase())} 130 |
131 |
132 | ) 133 | } 134 | } 135 | 136 | export default withTranslation('common')(NetatmoModuleGraph) 137 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoStation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, ButtonGroup, Colors} from "@blueprintjs/core"; 3 | import { withTranslation, WithTranslation } from 'react-i18next'; 4 | import * as i18next from 'i18next'; 5 | import {Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis} from "recharts"; 6 | import ModuleLayout from "../layouts/ModuleLayout"; 7 | import { INetatmoNAMain } from "../models/NetatmoNAMain"; 8 | import * as netatmoActions from "../store/netatmo/actions"; 9 | import {ConnectedReduxProps} from "../store"; 10 | import {Orientation} from "../store/application/types"; 11 | import {colorChooser} from "../utils/tools"; 12 | import {DataTypes, Timelapse} from "../types/netatmo"; 13 | 14 | // Separate state props + dispatch props to their own interfaces. 15 | interface IPropsFromState { 16 | station_data: INetatmoNAMain|undefined 17 | selected_timelapse: Timelapse 18 | temperature_unit: string 19 | pressure_unit: string 20 | orientation: Orientation 21 | selected_type: DataTypes 22 | measure_data: [] 23 | } 24 | 25 | // We can use `typeof` here to map our dispatch types to the props, like so. 26 | interface IPropsFromDispatch extends WithTranslation { 27 | [key: string]: any 28 | fetchMeasure: typeof netatmoActions.fetchMeasure 29 | onChangeSelectedType: typeof netatmoActions.onChangeSelectedType 30 | t: i18next.TFunction 31 | } 32 | 33 | // Combine both state + dispatch props - as well as any props we want to pass - in a union type. 34 | type AllProps = IPropsFromState & IPropsFromDispatch & ConnectedReduxProps; 35 | 36 | /** Main station */ 37 | const NetatmoModuleStation: React.FunctionComponent = (props) => { 38 | const _onClick = (type: string) => { 39 | if (props.orientation !== 'portrait') { 40 | props.fetchMeasure(props.station_data?.id as string, props.station_data?.id as string, [type], props.selected_timelapse); 41 | } 42 | } 43 | 44 | return ( 45 | 52 |
53 |
54 |
_onClick('Temperature')}> 55 |
{props.t('netatmo.temperature')}
56 | {props.station_data?.data?.temperature}°{props.temperature_unit} 57 |
58 | { 59 | props.orientation === 'portrait' && ( 60 |
_onClick('Pressure')} style={{textAlign: 'center'}}> 61 |
{props.t('netatmo.barometer')}
62 | {props.station_data?.data?.pressure}{props.pressure_unit} 63 |
64 | ) 65 | } 66 |
_onClick('Humidity')} style={{textAlign: 'right'}}> 67 |
{props.t('netatmo.humidity')}
68 | {props.station_data?.data?.humidity}% 69 |
70 |
71 |
72 |
_onClick('CO2')}> 73 |
co2
74 | {props.station_data?.data?.co2}ppm 75 |
76 |
_onClick('Noise')} style={{textAlign: 'right'}}> 77 |
{props.t('netatmo.noise')}
78 | {props.station_data?.data?.noise}dB 79 |
80 |
81 | { 82 | props.orientation === 'portrait' && ( 83 | <> 84 | 85 | { 86 | props.station_data?.data_type.map((type, index) => 87 | 92 | ) 93 | } 94 | 95 | 96 | 101 | 102 | 103 | 104 | 113 | 114 | 115 | 116 | ) 117 | } 118 |
119 |
120 | ) 121 | }; 122 | 123 | export default withTranslation('common')(NetatmoModuleStation) 124 | -------------------------------------------------------------------------------- /public/bundle.437619a9224327bb3054.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2017 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! 14 | Copyright (C) 2013-2015 by Andrea Giammarchi - @WebReflection 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in 24 | all copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 32 | THE SOFTWARE. 33 | 34 | */ 35 | 36 | /*! Conditions:: INITIAL */ 37 | 38 | /*! Production:: $accept : expression $end */ 39 | 40 | /*! Production:: css_value : ANGLE */ 41 | 42 | /*! Production:: css_value : CHS */ 43 | 44 | /*! Production:: css_value : EMS */ 45 | 46 | /*! Production:: css_value : EXS */ 47 | 48 | /*! Production:: css_value : FREQ */ 49 | 50 | /*! Production:: css_value : LENGTH */ 51 | 52 | /*! Production:: css_value : PERCENTAGE */ 53 | 54 | /*! Production:: css_value : REMS */ 55 | 56 | /*! Production:: css_value : RES */ 57 | 58 | /*! Production:: css_value : SUB css_value */ 59 | 60 | /*! Production:: css_value : TIME */ 61 | 62 | /*! Production:: css_value : VHS */ 63 | 64 | /*! Production:: css_value : VMAXS */ 65 | 66 | /*! Production:: css_value : VMINS */ 67 | 68 | /*! Production:: css_value : VWS */ 69 | 70 | /*! Production:: css_variable : CSS_VAR */ 71 | 72 | /*! Production:: expression : math_expression EOF */ 73 | 74 | /*! Production:: math_expression : LPAREN math_expression RPAREN */ 75 | 76 | /*! Production:: math_expression : NESTED_CALC LPAREN math_expression RPAREN */ 77 | 78 | /*! Production:: math_expression : SUB PREFIX SUB NESTED_CALC LPAREN math_expression RPAREN */ 79 | 80 | /*! Production:: math_expression : css_value */ 81 | 82 | /*! Production:: math_expression : css_variable */ 83 | 84 | /*! Production:: math_expression : math_expression ADD math_expression */ 85 | 86 | /*! Production:: math_expression : math_expression DIV math_expression */ 87 | 88 | /*! Production:: math_expression : math_expression MUL math_expression */ 89 | 90 | /*! Production:: math_expression : math_expression SUB math_expression */ 91 | 92 | /*! Production:: math_expression : value */ 93 | 94 | /*! Production:: value : NUMBER */ 95 | 96 | /*! Production:: value : SUB NUMBER */ 97 | 98 | /*! Rule:: $ */ 99 | 100 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)% */ 101 | 102 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)Hz\b */ 103 | 104 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)\b */ 105 | 106 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ch\b */ 107 | 108 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)cm\b */ 109 | 110 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)deg\b */ 111 | 112 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dpcm\b */ 113 | 114 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dpi\b */ 115 | 116 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dppx\b */ 117 | 118 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)em\b */ 119 | 120 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ex\b */ 121 | 122 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)grad\b */ 123 | 124 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)in\b */ 125 | 126 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)kHz\b */ 127 | 128 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)mm\b */ 129 | 130 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ms\b */ 131 | 132 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)pc\b */ 133 | 134 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)pt\b */ 135 | 136 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)px\b */ 137 | 138 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)rad\b */ 139 | 140 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)rem\b */ 141 | 142 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)s\b */ 143 | 144 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)turn\b */ 145 | 146 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vh\b */ 147 | 148 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vmax\b */ 149 | 150 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vmin\b */ 151 | 152 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vw\b */ 153 | 154 | /*! Rule:: ([a-z]+) */ 155 | 156 | /*! Rule:: (calc) */ 157 | 158 | /*! Rule:: (var\([^\)]*\)) */ 159 | 160 | /*! Rule:: - */ 161 | 162 | /*! Rule:: \( */ 163 | 164 | /*! Rule:: \) */ 165 | 166 | /*! Rule:: \* */ 167 | 168 | /*! Rule:: \+ */ 169 | 170 | /*! Rule:: \/ */ 171 | 172 | /*! Rule:: \s+ */ 173 | 174 | /*! decimal.js-light v2.5.1 https://github.com/MikeMcl/decimal.js-light/LICENCE */ 175 | 176 | /*!@license Copyright 2013, Heinrich Goebl, License: MIT, see https://github.com/hgoebl/mobile-detect.js*/ 177 | 178 | /*!mobile-detect v1.4.4 2019-09-21*/ 179 | 180 | /** @license React v0.20.1 181 | * scheduler.production.min.js 182 | * 183 | * Copyright (c) Facebook, Inc. and its affiliates. 184 | * 185 | * This source code is licensed under the MIT license found in the 186 | * LICENSE file in the root directory of this source tree. 187 | */ 188 | 189 | /** @license React v16.13.1 190 | * react-is.production.min.js 191 | * 192 | * Copyright (c) Facebook, Inc. and its affiliates. 193 | * 194 | * This source code is licensed under the MIT license found in the 195 | * LICENSE file in the root directory of this source tree. 196 | */ 197 | 198 | /** @license React v17.0.1 199 | * react-dom.production.min.js 200 | * 201 | * Copyright (c) Facebook, Inc. and its affiliates. 202 | * 203 | * This source code is licensed under the MIT license found in the 204 | * LICENSE file in the root directory of this source tree. 205 | */ 206 | 207 | /** @license React v17.0.1 208 | * react.production.min.js 209 | * 210 | * Copyright (c) Facebook, Inc. and its affiliates. 211 | * 212 | * This source code is licensed under the MIT license found in the 213 | * LICENSE file in the root directory of this source tree. 214 | */ 215 | 216 | //! moment.js 217 | 218 | //! moment.js locale configuration 219 | -------------------------------------------------------------------------------- /src/css/style.scss: -------------------------------------------------------------------------------- 1 | @import "~weather-icons2/css/weather-icons.css"; 2 | @import "~weather-icons2/css/weather-icons-wind.css"; 3 | @import "~reboot.css/dist/reboot.css"; 4 | 5 | @import "~@blueprintjs/core/lib/css/blueprint.css"; 6 | @import "variables"; 7 | 8 | @font-face { 9 | font-family: 'Digital-7'; 10 | src: url('digital-7 (mono).eot'); 11 | src: url('digital-7 (mono).eot?#iefix') format('embedded-opentype'), 12 | url('digital-7 (mono).woff') format('woff'), 13 | url('digital-7 (mono).ttf') format('truetype'), 14 | url('digital-7 (mono).svg#Digital-7') format('svg'); 15 | } 16 | 17 | body { 18 | width: 100%; 19 | height: 100%; 20 | max-height: 100vh; 21 | user-select: none; 22 | //overflow: hidden; 23 | font-family: 'Digital-7',sans-serif; 24 | color: $white; 25 | font-size: 1.2rem; 26 | line-height: 1; 27 | background-color: $dark; 28 | position: absolute; 29 | background-image: url('../img/world_map_800x480.png'); 30 | //background-repeat: no-repeat; 31 | background-size: cover; 32 | //z-index: 1; 33 | } 34 | 35 | /** Module container **/ 36 | .module-container { 37 | position: relative; 38 | padding: 5px 10px; 39 | //max-width: 266px; 40 | //min-width: 180px; 41 | 42 | .toolbar { 43 | position: absolute; 44 | top: 4px; 45 | right: 16px; 46 | } 47 | 48 | .item-label { 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | right: 0; 53 | display: flex; 54 | 55 | .label { 56 | background-color: $blue; 57 | padding: 1px 6px; 58 | } 59 | 60 | .horizontal-top-divider { 61 | border-top: 1px solid $gray; 62 | flex: 1; 63 | margin: 0 0 0 4px; 64 | } 65 | 66 | .status { 67 | background-color: $gray; 68 | padding: 1px 6px; 69 | font-size: 1.0rem; 70 | 71 | .icon { 72 | padding-left: 2px; 73 | padding-right: 2px; 74 | } 75 | } 76 | } 77 | 78 | .vertical-right-divider { 79 | position: absolute; 80 | top: 0; 81 | right: 0; 82 | bottom: 0; 83 | border-right: 1px solid $gray; 84 | //margin: 8px 0 8px 0; 85 | } 86 | 87 | .sub-label { 88 | font-size: 0.9rem; 89 | } 90 | } 91 | 92 | /** Modules **/ 93 | .modules-layout { 94 | display: flex; 95 | flex-direction: column; 96 | padding-top: 22px; 97 | font-size: 2.6rem; 98 | 99 | small { 100 | font-size: 1.2rem; 101 | margin-left: 4px; 102 | } 103 | 104 | .row { 105 | display: flex; 106 | justify-content: space-between; 107 | 108 | .wind-orientation { 109 | flex: 1; 110 | display: flex; 111 | justify-content: center; 112 | align-items: center; 113 | font-size: 5rem; 114 | } 115 | } 116 | } 117 | 118 | .module-unreachable { 119 | display: flex; 120 | align-items: center; 121 | padding-top: 22px; 122 | 123 | .description { 124 | font-size: 0.8rem; 125 | margin-left: 12px; 126 | } 127 | } 128 | 129 | .module-datetime { 130 | display: flex; 131 | flex-direction: column; 132 | justify-content: center; 133 | align-items: center; 134 | padding-top: 4px; 135 | 136 | .time { 137 | font-size: 5.8rem; 138 | line-height: 0.8; 139 | padding-top: 20px; 140 | //font-weight: 700; 141 | 142 | small { 143 | margin-left: 4px; 144 | font-size: 2.9rem; 145 | } 146 | } 147 | 148 | .date { 149 | font-size: 1.4rem; 150 | } 151 | 152 | .sun { 153 | padding-top: 2px; 154 | width: 100%; 155 | display: flex; 156 | justify-content: space-evenly; 157 | } 158 | } 159 | 160 | .sunrise { 161 | font-size: 1rem; 162 | padding-bottom: 2px; 163 | } 164 | .sunset { 165 | font-size: 1rem; 166 | padding-bottom: 2px; 167 | text-align: right; 168 | } 169 | 170 | .module-forecast { 171 | display: flex; 172 | justify-content: space-between; 173 | align-items: center; 174 | padding-top: 20px; 175 | 176 | .module-forecast-daily { 177 | display: flex; 178 | flex-direction: column; 179 | align-items: center; 180 | min-width: 16.66%; 181 | //padding: 0 12px; 182 | //width: 82px; 183 | 184 | .daily-temperatures { 185 | display: flex; 186 | justify-content: space-around; 187 | padding-top: 4px; 188 | width: 100%; 189 | } 190 | 191 | .icon { 192 | padding-top: 12px; 193 | padding-bottom: 10px; 194 | } 195 | } 196 | } 197 | 198 | .station-name { 199 | margin-right: 12px; 200 | font-size: 1.8rem; 201 | } 202 | .last-update { 203 | //margin-right: 12px; 204 | font-size: 1rem; 205 | } 206 | 207 | /** Layouts **/ 208 | .main-layout { 209 | z-index: 10; 210 | height: 100vh; 211 | display: flex; 212 | } 213 | 214 | /** Layout for all modules **/ 215 | .dashboard-grid-layout { 216 | display: grid; 217 | grid-template-columns: 280px 520px; 218 | 219 | .first-column { 220 | display: grid; 221 | grid-template-rows: 176px 145px 85px 74px; 222 | } 223 | 224 | .second-column { 225 | display: grid; 226 | grid-template-rows: 130px 144px 178px 28px; 227 | 228 | .row { 229 | display: flex; 230 | } 231 | 232 | .row-grid { 233 | display: grid; 234 | grid-template-columns: 220px 300px; 235 | } 236 | } 237 | } 238 | 239 | .dashboard-layout { 240 | display: flex; 241 | flex-direction: column; 242 | flex-wrap: wrap; 243 | //flex: 1; 244 | } 245 | 246 | /** Starting page **/ 247 | .starting-page-layout { 248 | display: flex; 249 | flex: 1; 250 | flex-direction: column; 251 | justify-content: flex-end; 252 | align-items: center; 253 | 254 | .content { 255 | display: flex; 256 | flex-direction: column; 257 | flex: 1; 258 | align-items: center; 259 | justify-content: center; 260 | width: 100%; 261 | 262 | .loader { 263 | display: flex; 264 | align-items: center; 265 | padding: 36px; 266 | height: 132px; 267 | } 268 | 269 | .title { 270 | text-transform: uppercase; 271 | } 272 | 273 | .description { 274 | width: 400px; 275 | text-align: center; 276 | } 277 | } 278 | 279 | .footer { 280 | height: 28px; 281 | font-size: 1rem; 282 | } 283 | } 284 | 285 | /* 286 | Lower than iphone XR 287 | */ 288 | @media only screen and (max-width: 896px) and (max-height: 414px) and (orientation: landscape), only screen and (max-width: 414px) and (max-height: 896px) and (orientation: portrait) { 289 | body { 290 | font-size: 1.1rem; 291 | } 292 | 293 | .modules-layout { 294 | padding-top: 22px; 295 | font-size: 2rem; 296 | 297 | small { 298 | font-size: 1.2rem; 299 | margin-left: 4px; 300 | } 301 | 302 | .row { 303 | .wind-orientation { 304 | font-size: 4rem; 305 | } 306 | } 307 | } 308 | 309 | .module-datetime { 310 | display: flex; 311 | 312 | .time { 313 | padding-top: 10px; 314 | font-size: 3.6rem; 315 | 316 | small { 317 | font-size: 1.8rem; 318 | } 319 | } 320 | 321 | .date { 322 | font-size: 1rem; 323 | } 324 | } 325 | 326 | .sunrise { 327 | font-size: 0.9rem; 328 | } 329 | .sunset { 330 | font-size: 0.9rem; 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/components/ModuleNetatmoIndoor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Alignment, Button, ButtonGroup, Colors} from "@blueprintjs/core"; 3 | import { withTranslation, WithTranslation } from 'react-i18next'; 4 | import * as i18next from 'i18next'; 5 | import { AreaChart, Area, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from 'recharts'; 6 | import ModuleLayout from "../layouts/ModuleLayout"; 7 | import { INetatmoNAModule4 } from "../models/NetatmoNAModule4"; 8 | import * as netatmoActions from "../store/netatmo/actions"; 9 | import {ConnectedReduxProps} from "../store"; 10 | import {Orientation} from "../store/application/types"; 11 | import {colorChooser} from "../utils/tools"; 12 | import {IIndoorModuleNames} from "../models/NetatmoNAMain"; 13 | import {DataTypes, Timelapse} from "../types/netatmo"; 14 | 15 | // Separate state props + dispatch props to their own interfaces. 16 | interface IPropsFromState { 17 | module_data: INetatmoNAModule4|undefined 18 | device_id: string|undefined 19 | selected_timelapse: Timelapse 20 | temperature_unit: string 21 | orientation: Orientation 22 | selected_type: DataTypes 23 | measure_data: [] 24 | module_name: string 25 | number_of_additional_modules?: number 26 | selected_indoor_module: number 27 | indoor_module_names: IIndoorModuleNames 28 | } 29 | 30 | // We can use `typeof` here to map our dispatch types to the props, like so. 31 | interface IPropsFromDispatch extends WithTranslation { 32 | [key: string]: any 33 | fetchMeasure: typeof netatmoActions.fetchMeasure 34 | onChangeSelectedType: typeof netatmoActions.onChangeSelectedType 35 | onChangeSelectedInsideModule: typeof netatmoActions.onChangeSelectedInsideModule 36 | t: i18next.TFunction 37 | } 38 | 39 | // Combine both state + dispatch props - as well as any props we want to pass - in a union type. 40 | type AllProps = IPropsFromState & IPropsFromDispatch & ConnectedReduxProps; 41 | 42 | /** Outdoor module */ 43 | const NetatmoModuleIndoor: React.FunctionComponent = (props) => { 44 | const _onClick = (type: string) => { 45 | if (props.orientation !== 'portrait') { 46 | props.fetchMeasure(props.device_id as string, props.module_data?.id as string, [type], props.selected_timelapse); 47 | } 48 | } 49 | 50 | return ( 51 | 64 |
65 |
66 |
_onClick('Temperature')}> 67 |
{props.t('netatmo.temperature')}
68 | {props.module_data?.data?.temperature}°{props.temperature_unit} 69 |
70 | { 71 | props.orientation === 'portrait' && ( 72 |
_onClick('CO2')} style={{textAlign: 'center'}}> 73 |
co2
74 | {props.module_data?.data?.co2}ppm 75 |
76 | ) 77 | } 78 |
_onClick('Humidity')} style={{textAlign: 'right'}}> 79 |
{props.t('netatmo.humidity')}
80 | {props.module_data?.data?.humidity}% 81 |
82 |
83 | { 84 | props.orientation !== 'portrait' && ( 85 |
86 |
_onClick('CO2')}> 87 |
co2
88 | {props.module_data?.data?.co2}ppm 89 |
90 | 91 |
92 | ) 93 | } 94 | { 95 | props.orientation === 'portrait' && ( 96 | <> 97 | 98 | { 99 | props.module_data?.data_type.map((type, index) => 100 | 105 | ) 106 | } 107 | 108 | 109 | 114 | 115 | 116 | 117 | 124 | 125 | 126 | 127 | ) 128 | } 129 |
130 |
131 | ) 132 | }; 133 | 134 | export default withTranslation('common')(NetatmoModuleIndoor) 135 | --------------------------------------------------------------------------------