New upstream version 22.9.0
Thorsten Alteholz
1 year, 6 months ago
0 | 0 | pywws - Python software for USB Wireless Weather Stations |
1 | 1 | http://github.com/jim-easterbrook/pywws |
2 | Copyright (C) 2008-21 pywws contributors | |
2 | Copyright (C) 2008-22 pywws contributors | |
3 | 3 | |
4 | 4 | This program is free software; you can redistribute it and/or |
5 | 5 | modify it under the terms of the GNU General Public License |
14 | 14 | You should have received a copy of the GNU General Public License |
15 | 15 | along with this program; if not, write to the Free Software |
16 | 16 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | ||
18 | Changes in v22.9.0: | |
19 | 1/ Reject data if 'pointer' value is invalid due to failing hardware. | |
20 | 2/ Updated Weather Underground uploader documentation. | |
21 | ||
22 | Changes in v22.3.0: | |
23 | 1/ Replace pytz with dateutil.tz. | |
24 | 2/ Fix some hidapi USB problems. | |
25 | 3/ Correct some typos in documentation. | |
17 | 26 | |
18 | 27 | Changes in v21.4.0: |
19 | 28 | 1/ Fixed problems with timezones ready for next release of tzlocal. |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: pywws |
2 | Version: 21.4.0 | |
2 | Version: 22.9.0 | |
3 | 3 | Summary: Python software for wireless weather stations |
4 | 4 | Home-page: http://jim-easterbrook.github.com/pywws/ |
5 | 5 | Author: Jim Easterbrook |
6 | 6 | Author-email: jim@jim-easterbrook.me.uk |
7 | 7 | License: GNU GPL |
8 | Download-URL: https://pypi.python.org/pypi/pywws/21.4.0 | |
8 | Download-URL: https://pypi.python.org/pypi/pywws/22.9.0 | |
9 | 9 | Description: .. pywws - Python software for USB Wireless Weather Stations |
10 | 10 | http://github.com/jim-easterbrook/pywws |
11 | 11 | Copyright (C) 2008-18 pywws contributors |
121 | 121 | Classifier: Programming Language :: Python |
122 | 122 | Classifier: Programming Language :: Python :: 2.7 |
123 | 123 | Classifier: Programming Language :: Python :: 3 |
124 | Provides-Extra: daemon | |
124 | 125 | Provides-Extra: sftp |
125 | Provides-Extra: daemon | |
126 | 126 | Provides-Extra: twitter |
0 | 0 | # pywws - Python software for USB Wireless Weather Stations |
1 | 1 | # http://github.com/jim-easterbrook/pywws |
2 | # Copyright (C) 2008-18 pywws contributors | |
2 | # Copyright (C) 2008-22 pywws contributors | |
3 | 3 | |
4 | 4 | # This program is free software; you can redistribute it and/or |
5 | 5 | # modify it under the terms of the GNU General Public License |
207 | 207 | 'pywws-version = pywws.version:main', |
208 | 208 | ], |
209 | 209 | }, |
210 | install_requires = ['tzlocal'], | |
210 | install_requires = ['python-dateutil'], | |
211 | 211 | extras_require = { |
212 | 212 | 'daemon' : ['python-daemon == 2.1.2'], |
213 | 213 | 'sftp' : ['paramiko', 'pycrypto'], |
42 | 42 | Assumptions |
43 | 43 | ----------- |
44 | 44 | |
45 | There are a number of assumptions that have been made to make this work which will directly affect its useability. These assumptions however have not been made available from Environment Canada, who are the original developers of the Humidex used in the PYWWS function cadhumidex. It is safe enough however to say that the following would have been some assumptions: | |
45 | There are a number of assumptions that have been made to make this work which will directly affect its usability. These assumptions however have not been made available from Environment Canada, who are the original developers of the Humidex used in the PYWWS function cadhumidex. It is safe enough however to say that the following would have been some assumptions: | |
46 | 46 | |
47 | 47 | * Clothing type, thickness |
48 | 48 | * Skin area exposed to free air |
0 | __version__ = '21.4.0' | |
1 | _release = '1690' | |
2 | _commit = 'dbede59' | |
0 | __version__ = '22.9.0' | |
1 | _release = '1696' | |
2 | _commit = 'e9ef20a' |
8 | 8 | python -m pywws.datastoretransfer filedata c:\weather_data sqlite3data d:\weather |
9 | 9 | |
10 | 10 | This can be used to convert from the default file base storage system to an |
11 | SQL based sorage system, or back. The tranfer will overwrite existing data | |
11 | SQL based sorage system, or back. The transfer will overwrite existing data | |
12 | 12 | in place which may leave existing data in the destination if the incoming data |
13 | 13 | does not overlap (i.e. source data is newer than the destination). This is a |
14 | 14 | risky way to merge datastores together. Otherwise, its recommended to use the |
15 | 15 | optional -c argument to ensure the destination is cleared first. |
16 | 16 | You may choose the same storage module for both source and destination |
17 | with different directories, and this is the equivelent of simply copying the | |
17 | with different directories, and this is the equivalent of simply copying the | |
18 | 18 | data but will build the underlying files from scratch. However, copying the |
19 | 19 | files by hand is likely to be faster. |
20 | 20 |
140 | 140 | :rtype: bool |
141 | 141 | |
142 | 142 | """ |
143 | data = ''.join(map(chr, buf)) | |
143 | data = bytes(buf) | |
144 | 144 | size = len(data) |
145 | 145 | if hidapi.hid_write(self.device, ctypes.c_char_p(data), size) != size: |
146 | 146 | raise IOError( |
50 | 50 | |
51 | 51 | __docformat__ = "restructuredtext en" |
52 | 52 | |
53 | from contextlib import contextmanager | |
54 | ||
53 | 55 | import hid |
54 | 56 | |
55 | 57 | class USBDevice(object): |
68 | 70 | def __init__(self, idVendor, idProduct): |
69 | 71 | if not hid.enumerate(idVendor, idProduct): |
70 | 72 | raise IOError("No weather station connected") |
73 | self.idVendor = idVendor | |
74 | self.idProduct = idProduct | |
71 | 75 | self.hid = hid.device(idVendor, idProduct) |
76 | ||
77 | @contextmanager | |
78 | def open(self): | |
79 | try: | |
80 | self.hid.open(self.idVendor, self.idProduct) | |
81 | yield | |
82 | finally: | |
83 | self.hid.close() | |
72 | 84 | |
73 | 85 | def read_data(self, size): |
74 | 86 | """Receive data from the device. |
86 | 98 | |
87 | 99 | """ |
88 | 100 | result = list() |
89 | while size > 0: | |
90 | count = min(size, 8) | |
91 | buf = self.hid.read(count) | |
92 | if len(buf) < count: | |
93 | raise IOError( | |
94 | 'pywws.device_cython_hidapi.USBDevice.read_data failed') | |
95 | result += buf | |
96 | size -= count | |
101 | with self.open(): | |
102 | while size > 0: | |
103 | count = min(size, 8) | |
104 | buf = self.hid.read(count) | |
105 | if len(buf) < count: | |
106 | raise IOError( | |
107 | 'pywws.device_cython_hidapi.USBDevice.read_data failed') | |
108 | result += buf | |
109 | size -= count | |
97 | 110 | return result |
98 | 111 | |
99 | 112 | def write_data(self, buf): |
108 | 121 | :rtype: bool |
109 | 122 | |
110 | 123 | """ |
111 | if self.hid.write(buf) != len(buf): | |
112 | raise IOError( | |
113 | 'pywws.device_cython_hidapi.USBDevice.write_data failed') | |
124 | with self.open(): | |
125 | if self.hid.write(buf) != len(buf): | |
126 | raise IOError( | |
127 | 'pywws.device_cython_hidapi.USBDevice.write_data failed') | |
114 | 128 | return True |
15 | 15 | # along with this program; if not, write to the Free Software |
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | 17 | |
18 | """Routines to perform common tasks such as plotting gaphs or uploading files.""" | |
18 | """Routines to perform common tasks such as plotting graphs or uploading files.""" | |
19 | 19 | |
20 | 20 | from __future__ import absolute_import |
21 | 21 |
40 | 40 | [logged] |
41 | 41 | services = ['mqtt', 'underground'] |
42 | 42 | |
43 | * To customize the MQTT message use template_txt (remove illuminace and uv if weather station does not support them):: | |
43 | * To customize the MQTT message use template_txt (remove illuminance and uv if weather station does not support them):: | |
44 | 44 | |
45 | 45 | [mqtt] |
46 | 46 | ... (as above) |
0 | 0 | # pywws - Python software for USB Wireless Weather Stations |
1 | 1 | # http://github.com/jim-easterbrook/pywws |
2 | # Copyright (C) 2018 pywws contributors | |
2 | # Copyright (C) 2018-22 pywws contributors | |
3 | 3 | |
4 | 4 | # This program is free software; you can redistribute it and/or |
5 | 5 | # modify it under the terms of the GNU General Public License |
20 | 20 | `Weather Underground`_ may be the oldest and best known site gathering |
21 | 21 | data from amateur weather stations. |
22 | 22 | |
23 | * Create account: http://www.wunderground.com/members/signup.asp | |
24 | * API: `<http://wiki.wunderground.com/index.php/PWS_-_Upload_Protocol>`_ | |
23 | * Create account: https://www.wunderground.com/signup | |
24 | * API: `<https://support.weather.com/s/article/PWS-Upload-Protocol>`_ | |
25 | 25 | * Additional dependency: http://docs.python-requests.org/ |
26 | 26 | * Example ``weather.ini`` configuration:: |
27 | 27 | |
38 | 38 | |
39 | 39 | The ``internal`` configuration setting allows you to include indoor |
40 | 40 | temperature and humidity in your uploads. |
41 | ||
42 | Note that ``password`` is not the password you use to log in to Weather | |
43 | Underground, it's the ``Key`` value shown on your list of devices: | |
44 | https://www.wunderground.com/member/devices | |
41 | 45 | |
42 | 46 | Previous versions of pywws had an extra ``underground_rf`` service to |
43 | 47 | use Weather Underground's "rapid fire" server for frequent uploads. Now |
108 | 108 | return ts |
109 | 109 | |
110 | 110 | def _adapt_WSStatus(status): |
111 | """Return integer represending WSStatus dictionary input""" | |
111 | """Return integer representing WSStatus dictionary input""" | |
112 | 112 | return int(WSStatus.to_csv(status)) |
113 | 113 | |
114 | 114 | # Data type convert SQLite3 ==> Python |
155 | 155 | table = "" |
156 | 156 | _keycol = "idx" |
157 | 157 | if len(conv) == 0: |
158 | raise KeyError("No coloumns are defined.") | |
158 | raise KeyError("No columns are defined.") | |
159 | 159 | if _keycol not in key_list: |
160 | # Check that the key coloumn is present | |
160 | # Check that the key column is present | |
161 | 161 | raise KeyError( |
162 | "Key coloumn '{}' is not in the key list".format(keycol) | |
162 | "Key column '{}' is not in the key list".format(keycol) | |
163 | 163 | ) |
164 | 164 | |
165 | 165 | def __init__(self, dir_name): |
184 | 184 | # as it will choose the smallest integer representation between 8-64bit, |
185 | 185 | # while floats are always 64bit. Set idx as a unique integer primary |
186 | 186 | # key so searches are faster, storage requirements smaller, |
187 | # the rowid coloumn is eliminated so there is no need for secondary | |
187 | # the rowid column is eliminated so there is no need for secondary | |
188 | 188 | # indices. Suitable converters/adapters are then applied. |
189 | 189 | if con.execute( |
190 | 190 | """SELECT COUNT(*) FROM sqlite_master |
193 | 193 | ).fetchone()[0] == 0: |
194 | 194 | con.executescript( |
195 | 195 | """CREATE TABLE IF NOT EXISTS {table} ( |
196 | {keycol} INTEGER PRIMARY KEY, {coloumns} );""".format( | |
196 | {keycol} INTEGER PRIMARY KEY, {columns} );""".format( | |
197 | 197 | table=table, |
198 | 198 | keycol=keycol, |
199 | coloumns=", ".join( | |
199 | columns=", ".join( | |
200 | 200 | "{} NUM".format(key) |
201 | 201 | for key in conv |
202 | 202 | if key != keycol |
204 | 204 | ) |
205 | 205 | ) |
206 | 206 | |
207 | # Get all coloumns from the database | |
207 | # Get all columns from the database | |
208 | 208 | sql_key_list = tuple( |
209 | 209 | (row["name"],row["pk"]) |
210 | 210 | for row in con.execute( |
222 | 222 | |
223 | 223 | # Convert this to just a set of keys |
224 | 224 | sql_key_list = set(key[0] for key in sql_key_list) |
225 | # Check that no coloumns are missing | |
225 | # Check that no columns are missing | |
226 | 226 | if not set(conv.keys()) <= sql_key_list: |
227 | 227 | raise KeyError( |
228 | "Mismatch between database coloumns and what was expected" | |
229 | ) | |
230 | ||
231 | # SQL snippet which casts all coloumns to the correct data types | |
228 | "Mismatch between database columns and what was expected" | |
229 | ) | |
230 | ||
231 | # SQL snippet which casts all columns to the correct data types | |
232 | 232 | # for SELECT * type queries |
233 | 233 | self.selallcols = ", ".join( |
234 | 234 | '{col} AS "{col} [{conv}]"'.format( |
236 | 236 | conv=conv[col] |
237 | 237 | ) for col in key_list |
238 | 238 | ) |
239 | # SQL snippet which casts the key coloum to the correct data type | |
239 | # SQL snippet which casts the key column to the correct data type | |
240 | 240 | # for SELECT {keycol} type queries |
241 | 241 | self.selkeycol = '{keycol} AS "{keycol} [{conv}]"'.format( |
242 | 242 | keycol=keycol, |
267 | 267 | """ |
268 | 268 | # Assuming the database has been analyzed before, the stat1 table |
269 | 269 | # should contain the total row count for the table as the first number |
270 | # in the stat coloumn from when it was last analyzed. Very fast but may | |
270 | # in the stat column from when it was last analyzed. Very fast but may | |
271 | 271 | # not be up to date |
272 | 272 | try: |
273 | 273 | return int(self._connection.execute( |
298 | 298 | "Start index is greater than the End index" |
299 | 299 | ) |
300 | 300 | else: |
301 | # Substitution of the key coloumn, but the | |
301 | # Substitution of the key column, but the | |
302 | 302 | # parameters themselves will be substituted by sqlite3 |
303 | 303 | predicate = "WHERE {} BETWEEN :start AND :stop".format( |
304 | 304 | self._keycol |
310 | 310 | # i.start will also be None |
311 | 311 | predicate = "WHERE {} <= :stop".format(self._keycol) |
312 | 312 | else: |
313 | # both are None, so equivelent to wanting everything | |
313 | # both are None, so equivalent to wanting everything | |
314 | 314 | predicate = "" |
315 | 315 | multi = True |
316 | 316 | pred = {"start": i.start, "stop": i.stop} |
317 | 317 | elif isinstance(i, datetime): |
318 | # Substitution of the key coloumn, but the | |
318 | # Substitution of the key column, but the | |
319 | 319 | # parameters themselves will be substituted by sqlite3 |
320 | 320 | predicate = "WHERE {} = :key".format(self._keycol) |
321 | 321 | multi = False |
386 | 386 | items being overwritten. |
387 | 387 | |
388 | 388 | Elements in E are assumed to be dicts containing the primary key to |
389 | allow the equivelent of: | |
389 | allow the equivalent of: | |
390 | 390 | for k in E: D[k.primary_key] = k |
391 | 391 | """ |
392 | 392 | key_list = self.key_list |
486 | 486 | ) |
487 | 487 | |
488 | 488 | def __iter__(self): |
489 | """Iterates over all rows in ascending order of key coloumn""" | |
489 | """Iterates over all rows in ascending order of key column""" | |
490 | 490 | for row in self._connection.execute( |
491 | 491 | """SELECT {} FROM {} ORDER BY {} ASC;""".format( |
492 | 492 | self.selallcols, |
497 | 497 | yield dict(row) |
498 | 498 | |
499 | 499 | def __reversed__(self): |
500 | """Iterates over all rows in decending order of key coloumn""" | |
500 | """Iterates over all rows in decending order of key column""" | |
501 | 501 | for row in self._connection.execute( |
502 | 502 | """SELECT {} FROM {} ORDER BY {} DESC;""".format( |
503 | 503 | self.selallcols, |
1 | 1 | |
2 | 2 | # pywws - Python software for USB Wireless Weather Stations |
3 | 3 | # http://github.com/jim-easterbrook/pywws |
4 | # Copyright (C) 2008-21 pywws contributors | |
4 | # Copyright (C) 2008-22 pywws contributors | |
5 | 5 | |
6 | 6 | # This program is free software; you can redistribute it and/or |
7 | 7 | # modify it under the terms of the GNU General Public License |
40 | 40 | import logging |
41 | 41 | import sys |
42 | 42 | |
43 | import tzlocal | |
43 | import dateutil.tz | |
44 | 44 | |
45 | 45 | from pywws.constants import DAY, HOUR |
46 | 46 | |
48 | 48 | |
49 | 49 | |
50 | 50 | class _TimeZone(object): |
51 | class _UTC(datetime.tzinfo): | |
52 | _offset = datetime.timedelta(0) | |
53 | ||
54 | def utcoffset(self, dt): | |
55 | return self._offset | |
56 | ||
57 | def dst(self, dt): | |
58 | return self._offset | |
59 | ||
60 | def tzname(self, dt): | |
61 | return 'UTC' | |
62 | ||
63 | ||
64 | 51 | def __init__(self, tz_name=None): |
65 | if tz_name: | |
66 | # use a named time zone instead of system default, for testing | |
67 | import pytz | |
68 | self.local = pytz.timezone(tz_name) | |
69 | else: | |
70 | self.local = tzlocal.get_localzone() | |
52 | self.local = dateutil.tz.gettz(tz_name) | |
71 | 53 | logger.info('Using timezone "{!s}"'.format(self.local)) |
72 | self._using_pytz = hasattr(self.local, 'localize') | |
73 | if sys.version_info >= (3, 2): | |
74 | self.utc = datetime.timezone.utc | |
75 | else: | |
76 | self.utc = self._UTC() | |
54 | self.utc = dateutil.tz.UTC | |
77 | 55 | |
78 | 56 | def local_to_utc(self, dt): |
79 | 57 | """Convert a local time (with or without tzinfo) to UTC without |
80 | 58 | tzinfo, as used for pywws timestamps.""" |
81 | 59 | if dt.tzinfo is None: |
82 | if self._using_pytz: | |
83 | dt = self.local.localize(dt) | |
84 | else: | |
85 | dt = dt.replace(tzinfo=self.local) | |
60 | dt = dt.replace(tzinfo=self.local) | |
86 | 61 | return dt.astimezone(self.utc).replace(tzinfo=None) |
87 | 62 | |
88 | 63 | def utc_to_local(self, dt): |
140 | 115 | dst = day.dst() |
141 | 116 | for d in range(365): |
142 | 117 | day -= DAY |
143 | if self._using_pytz: | |
144 | day = self.local.normalize(day) | |
145 | 118 | if day.dst() == dst: |
146 | 119 | continue |
147 | 120 | hour = day |
148 | 121 | for h in range(25): |
149 | 122 | hour += HOUR |
150 | if self._using_pytz: | |
151 | hour = self.local.normalize(hour) | |
152 | 123 | if hour.dst() == dst: |
153 | 124 | yield hour |
154 | 125 | break |
0 | 0 | # pywws - Python software for USB Wireless Weather Stations |
1 | 1 | # http://github.com/jim-easterbrook/pywws |
2 | # Copyright (C) 2008-20 pywws contributors | |
2 | # Copyright (C) 2008-22 pywws contributors | |
3 | 3 | |
4 | 4 | # This program is free software; you can redistribute it and/or |
5 | 5 | # modify it under the terms of the GNU General Public License |
710 | 710 | self._read_fixed_block(0x0020), self.lo_fix_format['current_pos']) |
711 | 711 | if new_ptr == self._current_ptr: |
712 | 712 | return self._current_ptr |
713 | if (new_ptr - self.data_start) % self.reading_len[self.ws_type]: | |
714 | logger.error('invalid ptr value %06x', new_ptr) | |
715 | if self._current_ptr: | |
716 | return self._current_ptr | |
713 | 717 | if self._current_ptr and new_ptr != self.inc_ptr(self._current_ptr): |
714 | 718 | logger.error( |
715 | 719 | 'unexpected ptr change %06x -> %06x', self._current_ptr, new_ptr) |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: pywws |
2 | Version: 21.4.0 | |
2 | Version: 22.9.0 | |
3 | 3 | Summary: Python software for wireless weather stations |
4 | 4 | Home-page: http://jim-easterbrook.github.com/pywws/ |
5 | 5 | Author: Jim Easterbrook |
6 | 6 | Author-email: jim@jim-easterbrook.me.uk |
7 | 7 | License: GNU GPL |
8 | Download-URL: https://pypi.python.org/pypi/pywws/21.4.0 | |
8 | Download-URL: https://pypi.python.org/pypi/pywws/22.9.0 | |
9 | 9 | Description: .. pywws - Python software for USB Wireless Weather Stations |
10 | 10 | http://github.com/jim-easterbrook/pywws |
11 | 11 | Copyright (C) 2008-18 pywws contributors |
121 | 121 | Classifier: Programming Language :: Python |
122 | 122 | Classifier: Programming Language :: Python :: 2.7 |
123 | 123 | Classifier: Programming Language :: Python :: 3 |
124 | Provides-Extra: daemon | |
124 | 125 | Provides-Extra: sftp |
125 | Provides-Extra: daemon | |
126 | 126 | Provides-Extra: twitter |