├── .gitignore
├── NOTES
├── display-weather-usb-power.sh
├── display-weather.sh
├── COPYING
├── sci.py
├── aqi.py
├── weather_api.py
├── README.md
├── weather_script.py
├── weather-script-preprocess-landscape.svg
├── weather-script-preprocess-landscape-right.svg
└── weather-script-preprocess.svg
/.gitignore:
--------------------------------------------------------------------------------
1 | weather-script-output.*
2 | *.pyc
3 |
--------------------------------------------------------------------------------
/NOTES:
--------------------------------------------------------------------------------
1 | for x in *.svg; do
2 | cat $x | perl -pe 's/path/path id="'`basename $x .svg`'"/' | grep path;
3 | done > paste.svg
4 |
--------------------------------------------------------------------------------
/display-weather-usb-power.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | FOLDER="/mnt/base-us/weather"
4 | FILE="weather.png"
5 | URL="http://cn-vpn.grisge.info/weather/weather.png"
6 |
7 | cd $FOLDER
8 |
9 | ifconfig wlan0 >/dev/null 2>&1
10 | if [ $? -ne 0 ];then
11 | exit
12 | fi
13 |
14 | rm $FOLDER/$FILE
15 |
16 | wget $URL -O $FOLDER/$FILE
17 |
18 | if [ -e $FOLDER/$FILE ];then
19 | eips -f -g $FOLDER/$FILE
20 | fi
21 |
--------------------------------------------------------------------------------
/display-weather.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | FOLDER="/mnt/base-us/weather"
4 | FILE="weather.png"
5 | URL="http://cn-vpn.grisge.info/weather/weather.png"
6 |
7 | cd $FOLDER
8 |
9 | if [ $(gasgauge-info -s 2>/dev/null | sed -ne 's/\([0-9]\+\).*/\1/p') -le 10 ];
10 | then
11 | eips -f "Low battery, charge please"
12 | exit;
13 | fi
14 |
15 | # Wake Up
16 | lipc-set-prop com.lab126.powerd wakeUp 1
17 |
18 | sleep 10
19 |
20 | ifconfig wlan0 >/dev/null 2>&1
21 | if [ $? -ne 0 ];then
22 | exit
23 | fi
24 |
25 | rm $FOLDER/$FILE
26 |
27 | # Lock screen
28 | /usr/bin/powerd_test -p
29 |
30 | sleep 10
31 |
32 | wget $URL -O $FOLDER/$FILE
33 |
34 | if [ -e $FOLDER/$FILE ];then
35 | eips -f -g $FOLDER/$FILE
36 | fi
37 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Gris Ge
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/sci.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python2
2 | # Copyright (C) 2015 Gris Ge
3 | # This library is free software; you can redistribute it and/or
4 | # modify it under the terms of the GNU Lesser General Public
5 | # License as published by the Free Software Foundation; either
6 | # version 3 of the License, or (at your option) any later version.
7 | #
8 | # This library is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 | # Lesser General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU Lesser General Public
14 | # License along with this library; If not, see .
15 | #
16 | # Author: Gris Ge
17 | import urllib2
18 |
19 | _SCI_URL = 'http://hq.sinajs.cn/list=sh000001'
20 |
21 | def _fetch_html(url):
22 | request = urllib2.Request(url)
23 | return urllib2.urlopen(request).read()
24 |
25 | def sci_get():
26 | '''
27 | Return current Shanghai Composite Index and change percentage in list.
28 | Data is from parsed from sina
29 | '''
30 | html_content = _fetch_html(_SCI_URL)
31 | tmp_ar = html_content.split(',')
32 | cur = float(tmp_ar[3])
33 | pre = float(tmp_ar[2])
34 | sci = "%.2f" % cur
35 | sci_chg = "%.2f%%" % float( (cur - pre) / pre * 100)
36 | return (sci, sci_chg)
37 |
--------------------------------------------------------------------------------
/aqi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python2
2 | # Copyright (C) 2015 Gris Ge
3 | # This library is free software; you can redistribute it and/or
4 | # modify it under the terms of the GNU Lesser General Public
5 | # License as published by the Free Software Foundation; either
6 | # version 3 of the License, or (at your option) any later version.
7 | #
8 | # This library is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 | # Lesser General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU Lesser General Public
14 | # License along with this library; If not, see .
15 | #
16 | # Author: Gris Ge
17 | import urllib2
18 | import re
19 |
20 | _AQI_URL = 'http://aqicn.org/city//m'
21 | _AQI_DIV_NAME = 'aqi'
22 | _AQI_REGEX = re.compile('summary.+\+([0-9]+)\+')
23 | _HTTP_HEADER = {
24 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:42.0) "
25 | "Gecko/20100101 Firefox/42.0",
26 | "Accept": "text/html,application/xhtml+xml,application/xml;"
27 | "q=0.9,*/*;q=0.8",
28 | }
29 |
30 |
31 | def _fetch_html(url):
32 | request = urllib2.Request(url, headers=_HTTP_HEADER)
33 | return urllib2.urlopen(request).read()
34 |
35 |
36 | def aqi_get(city_name):
37 | '''
38 | Return integer for AQI of given city and raise error if failure.
39 | Data is from parsed from aqicn.org webpage.
40 | '''
41 | url = _AQI_URL.replace('', city_name)
42 | html_content = _fetch_html(url)
43 | for line in html_content.split("\n"):
44 | match = _AQI_REGEX.search(line)
45 | if match:
46 | return int(match.group(1))
47 | return -1
48 |
--------------------------------------------------------------------------------
/weather_api.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python2
2 | # Copyright (C) 2015 Gris Ge
3 | # This library is free software; you can redistribute it and/or
4 | # modify it under the terms of the GNU Lesser General Public
5 | # License as published by the Free Software Foundation; either
6 | # version 3 of the License, or (at your option) any later version.
7 | #
8 | # This library is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 | # Lesser General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU Lesser General Public
14 | # License along with this library; If not, see .
15 | #
16 | # Author: Gris Ge
17 | import datetime
18 | import json
19 | from urllib2 import urlopen
20 |
21 |
22 | def _fetch_json(url):
23 | json_str = urlopen(url).read()
24 | return json.loads(json_str)
25 |
26 |
27 | def _parse_forecast(data_json):
28 | """
29 | return [WeatherData]
30 | """
31 | tmp_list = []
32 | for data in data_json["forecast"]["simpleforecast"]["forecastday"]:
33 | tmp_list.append(WeatherData(data["icon"], data["high"]["celsius"],
34 | data["low"]["celsius"]))
35 | return tmp_list
36 |
37 |
38 | class WeatherData(object):
39 | def __init__(self, condition, temp_max, temp_min):
40 | self.condition = condition
41 | self.temp_max = temp_max
42 | self.temp_min = temp_min
43 |
44 |
45 | class WeatherAPI(object):
46 |
47 | _BASE_API_URL = "http://api.wunderground.com/api/"
48 |
49 | def __init__(self, api_key):
50 | self._api_key = api_key
51 | self._today = datetime.date.today()
52 |
53 | def set_lat_lon(self, lat, lon):
54 | forecast_json = _fetch_json(
55 | "%s/%s/forecast/q/%s,%s.json" %
56 | (WeatherAPI._BASE_API_URL, self._api_key, lat, lon))
57 |
58 | self._data = _parse_forecast(forecast_json)
59 |
60 | def set_airport_code(self, airport_code):
61 | forecast_json = _fetch_json(
62 | "%s/%s/forecast/q/%s.json" %
63 | (WeatherAPI._BASE_API_URL, self._api_key, airport_code))
64 |
65 | self._data = _parse_forecast(forecast_json)
66 |
67 |
68 | def temp_max(self, day):
69 | """
70 | Input day as integer, 0 means today, 1 means tomorrow, max is 3.
71 | """
72 | if day > 3:
73 | raise Exception("Invalid day, should less or equal to 3")
74 |
75 | return self._data[day].temp_max
76 |
77 | def temp_min(self, day):
78 | if day > 3:
79 | raise Exception("Invalid day, should less or equal to 3")
80 | return self._data[day].temp_min
81 |
82 | def condition(self, day):
83 | if day > 3:
84 | raise Exception("Invalid day, should less or equal to 3")
85 | return self._data[day].condition
86 |
87 | @property
88 | def today(self):
89 | """
90 | Return a object of datetime.date
91 | """
92 | return self._today
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Kindle Weather
2 |
3 | # STOP, THIS WILL BRICK YOUR KINDLE, ONLY FOR LINUX GEEK.
4 | # ~~UNLESS YOU KNOW ALL THE FOLLOWING LINUX COMMANDS, GO AWAY, THIS IS NOT FOR YOU.~~
5 |
6 | # Big Thanks to Matthew Petroff for his sharing.
7 |
8 | This project is fully based on the project shared by Matthew Petroff:
9 | http://mpetroff.net/2012/09/kindle-weather-display/
10 |
11 | Without his kind share of this great work, my kindle 3 might still sleep in the
12 | dust.
13 |
14 | The changes I made:
15 | * Use `crontab` instead of `kite` or stop `powerd` by changing the
16 | `suspend_levels`.
17 |
18 | * Reorganized the `weather_script.py` by spliting weather API out.
19 |
20 | * Change `weather-script-preprocess.svg` to fit the weather API.
21 |
22 | * Warn user when low battery(less than 10%)
23 |
24 | * I tried to change `weather-script-preprocess.svg` to use `` tag
25 | for external image which allow me to easily change the icon, but I failed to
26 | at the rsvg-convert point. So still in embedded way.
27 |
28 | # License
29 |
30 | * The code is in MIT license.
31 | * The `weather_script.py` file is based on the work of Matthew Petroff 2012, MIT
32 | license.
33 | * The `weather-script-preprocess.svg` and its embedded icon are under
34 | [CC0 Public Domain Dedication][4] license by Matthew Petroff also. I(Gris Ge)
35 | did small change to fit the [weather.com][1] API.
36 |
37 | # Pre-requirements
38 |
39 | * Kindle 3 wireless (Only tested on this one).
40 | * The API key from [weather.com][1]
41 | * [Kindle Jailbreak][2] and [USB network][3].
42 | * A kindle WIFI accessible Linux server with `python2 pngcrush librsvg2-tools`.
43 |
44 | # HowTo
45 | ## Kindle Side
46 |
47 | ### Jailbreak your Kindle and install USB network.
48 | * The above wiki pages works on me. The tips would be disable wifi at the
49 | initial login, then change root password, change ssh config to allow wifi
50 | login, set as auto start, reboot.
51 |
52 | ### Install weather display script
53 | * Edit the `URL` in `display-weather.sh` by pointing to your own http server.
54 | * Create folder `mkdir /mnt/base-us/weather`.
55 | * Make sure kindle root mount point is writeable `mntroot rw`
56 | * `scp display-weather.sh root@k3w:/usr/bin/`
57 | * Make sure `display-weather.sh` is executable via `chmod +x` on kindle.
58 |
59 | ### Setup crontab
60 | * Execute `mntroot rw`.
61 | * Add this line into `/etc/crontab/root`
62 |
63 | ```
64 | 5 6-22 * * * /usr/bin/display-weather.sh
65 | ```
66 | * Changed above line to suit your needs.
67 |
68 | ### Keep your kindle connected to a USB power during use.
69 | * I(Gris Ge) don't suggest your to hack kindle in order to stop the auto
70 | suspend, you might face batter drain. Please collect your kindle to a USB
71 | charger always.
72 |
73 | ### Reboot your kindle via `reboot` command.
74 |
75 | ## Server Side
76 | ### Install httpd daemon.
77 | * `sudo yum install httpd pngcrush librsvg2-tools python2 -y`
78 | * `sudo systemctl enable httpd`
79 | * `sudo systemctl start httpd`
80 |
81 | ### Create weather output folder with correct permission.
82 | * `sudo mkdir /var/www/html/weather`
83 | * `sudo chmod 777 /var/www/html/weather/` or use `chown`.
84 |
85 | ### Setup crontab
86 | * Create a script named as kindle_weather.sh like below:
87 |
88 | ```
89 | export KW_INCLUDE_SCI=1
90 | # ^ Include China Shanghai Composite index.
91 | export KW_LATITUDE=30.6586
92 | export KW_LONGTITUDE=104.0647
93 | # ^ If you have airport in your city, use something like `KW_AIRPORT=CTU`
94 | # instead
95 | export KW_WEATHER_KEY=""
96 | # ^ Weather.com API key
97 | export KW_AQI_CITY="chengdu"
98 | # ^ aqicn.org city name.
99 | export KW_LANSCAPE_LEFT=1
100 | # ^ Lanscape left mode(usb port at your right side).
101 | # KW_LANSCAPE_RIGHT for usb port at your left side.
102 | export KW_OUTPUT="/var/www/html/weather/weather.png"
103 |
104 | /home/fge/Source/kindle-weather/weather_script.py
105 | ```
106 |
107 | * Add this line to `crontab -e` to refresh weather every 30 minutes
108 | during 6AM to 10PM:
109 |
110 | ```
111 | 0,30 6-22 * * * /home/fge/bin/kindle_weather.sh 1>/dev/null 2>/dev/null
112 | ```
113 |
114 | ### Create initial weather PNG
115 | * Invoke `/weather_script.py `.
116 | * Check whether you can get the weather png via:
117 | `http:///weather/weather.png`
118 |
119 | # TODO
120 | * Include a TODO list or note.
121 |
122 | # Contact
123 | * https://github.com/cathay4t/kindle-weather/issues
124 |
125 | [1]: http://www.wunderground.com/weather/api/d/login.html
126 | [2]: http://wiki.mobileread.com/wiki/Kindle_Hacks_Information
127 | [3]: https://blitiri.com.ar/p/other/kindle/#usb-networking
128 | [4]: http://creativecommons.org/publicdomain/zero/1.0/
129 |
--------------------------------------------------------------------------------
/weather_script.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python2
2 | # Copyright (c) 2015 Gris Ge
3 | #
4 | # Permission is hereby granted, free of charge, to any person obtaining a copy
5 | # of this software and associated documentation files (the "Software"), to
6 | # deal in the Software without restriction, including without limitation the
7 | # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8 | # sell copies of the Software, and to permit persons to whom the Software is
9 | # furnished to do so, subject to the following conditions:
10 | #
11 | # The above copyright notice and this permission notice shall be included
12 | # in all copies or substantial portions of the Software.
13 | #
14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 | #
22 | #
23 | # Author: Gris Ge
24 |
25 | # Code was edit based on
26 | # https://github.com/mpetroff/kindle-weather-display.git
27 | # Which is also MIT license.
28 | #
29 | # Kindle Weather Display
30 | # Matthew Petroff (http://mpetroff.net/)
31 | # September 2012
32 |
33 | import codecs
34 | import datetime
35 | import os
36 | import sys
37 |
38 | from weather_api import WeatherAPI
39 | from argparse import ArgumentParser
40 | from aqi import aqi_get
41 | from sci import sci_get
42 |
43 |
44 | def _exec(cmd):
45 | rc = os.system(cmd)
46 | if (rc != 0):
47 | print("`%s` failed with error %d" % (cmd, rc))
48 | exit(rc)
49 |
50 | CODE_FOLDER = os.path.dirname(os.path.realpath(__file__))
51 |
52 | OUTPUT = os.environ.get("KW_OUTPUT", "/var/www/html/weather/weather.png")
53 |
54 | TMP_OUTPUT = "%s/weather.png" % CODE_FOLDER
55 | SVG_PORTRAIT_FILE = "%s/weather-script-preprocess.svg" % CODE_FOLDER
56 | SVG_LANSCAPE_LEFT = "%s/weather-script-preprocess-landscape.svg" % CODE_FOLDER
57 | SVG_LANSCAPE_RIGHT = "%s/weather-script-preprocess-landscape-right.svg" % \
58 | CODE_FOLDER
59 | SVG_FILE = SVG_PORTRAIT_FILE
60 | SVG_OUTPUT = "%s/weather-script-output.svg" % CODE_FOLDER
61 | MAX_WEATHER_DAY_COUNT = 3
62 | INCLUDE_SCI = False
63 |
64 | if len(sys.argv) >= 5 and sys.argv[4] != '0':
65 | SVG_FILE = SVG_LANSCAPE_FILE
66 |
67 | if os.environ.get("KW_LANSCAPE_RIGHT") is not None:
68 | SVG_FILE = SVG_LANSCAPE_RIGHT
69 | elif os.environ.get("KW_LANSCAPE_LEFT") is not None:
70 | SVG_FILE = SVG_LANSCAPE_LEFT
71 | else:
72 | SVG_FILE = SVG_PORTRAIT_FILE
73 |
74 |
75 | AQI_CITY = os.environ.get("KW_AQI_CITY", None)
76 |
77 | if os.environ.get("KW_INCLUDE_SCI") is not None:
78 | INCLUDE_SCI = True
79 |
80 | WEATHER_KEY = os.environ.get("KW_WEATHER_KEY")
81 | LATITUDE = os.environ.get("KW_LATITUDE")
82 | LONGTITUDE = os.environ.get("KW_LONGTITUDE")
83 | WEATHER_AIRPORT = os.environ.get("KW_AIRPORT")
84 |
85 | if WEATHER_KEY is None:
86 | print("Need KW_WEATHER_KEY environment variables")
87 | exit(1)
88 |
89 | weather_obj = WeatherAPI(WEATHER_KEY)
90 |
91 | if WEATHER_AIRPORT:
92 | weather_obj.set_airport_code(WEATHER_AIRPORT)
93 | else:
94 | weather_obj.set_lat_lon(LATITUDE, LONGTITUDE)
95 |
96 | # Open SVG to process
97 | output = codecs.open(SVG_FILE, "r", encoding="utf-8").read()
98 |
99 | _MAP = {
100 | "$I": WeatherAPI.condition,
101 | "$H": WeatherAPI.temp_max,
102 | "$L": WeatherAPI.temp_min,
103 | }
104 |
105 | for x in _MAP.keys():
106 | for i in range(MAX_WEATHER_DAY_COUNT + 1):
107 | output = output.replace("%s%d" % (x, i),
108 | "%s" % _MAP[x](weather_obj, i))
109 |
110 | # Replace refresh time
111 | output = output.replace("$TIME",
112 | datetime.datetime.now().strftime("%b %d %a %H:%M"))
113 |
114 | # Updaet AQI. TODO(Gris Ge): still place holder yet.
115 | if AQI_CITY is not None:
116 | output = output.replace("$AQI", str(aqi_get(AQI_CITY)))
117 |
118 | if INCLUDE_SCI:
119 | (sci, sci_change) = sci_get()
120 | output = output.replace("$SCI", str(sci))
121 | output = output.replace("$SCHG", str(sci_change))
122 | else:
123 | output = output.replace("SCI: $SCI $SCHG", "")
124 |
125 | day_one = weather_obj.today
126 |
127 | # Insert days of week
128 | one_day = datetime.timedelta(days=1)
129 | days_of_week = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
130 |
131 | for i in range(MAX_WEATHER_DAY_COUNT + 1):
132 | output = output.replace("$D%s" % i,
133 | days_of_week[(day_one + i * one_day).weekday()])
134 |
135 | # Write output
136 | codecs.open(SVG_OUTPUT, "w", encoding="utf-8").write(output)
137 |
138 | _exec("rsvg-convert --background-color=white -o %s %s" %
139 | (TMP_OUTPUT, SVG_OUTPUT))
140 | _exec("pngcrush -c 0 -ow %s 1>/dev/null 2>&1" % TMP_OUTPUT)
141 | _exec("mv -f '%s' '%s'" % (TMP_OUTPUT, OUTPUT))
142 |
--------------------------------------------------------------------------------
/weather-script-preprocess-landscape.svg:
--------------------------------------------------------------------------------
1 |
95 |
--------------------------------------------------------------------------------
/weather-script-preprocess-landscape-right.svg:
--------------------------------------------------------------------------------
1 |
95 |
--------------------------------------------------------------------------------
/weather-script-preprocess.svg:
--------------------------------------------------------------------------------
1 |
160 |
--------------------------------------------------------------------------------