From 396a248e10971204babbf1686461d6fad880d0bf Mon Sep 17 00:00:00 2001 From: "an.lazarev" Date: Tue, 8 Oct 2024 17:47:23 +0400 Subject: [PATCH] davisAPI full --- davisAPI/.vscode/settings.json | 5 + davisAPI/PyWeather-master/.gitignore | 5 + davisAPI/PyWeather-master/CHANGELOG.txt | 67 ++ davisAPI/PyWeather-master/COPYING.txt | 674 +++++++++++++++++ davisAPI/PyWeather-master/INSTALL.txt | 15 + davisAPI/PyWeather-master/MANIFEST.in | 3 + davisAPI/PyWeather-master/Makefile | 55 ++ davisAPI/PyWeather-master/README.md | 85 +++ .../scripts/weatherpub.conf.example | 23 + .../PyWeather-master/scripts/weatherpub.py | 240 ++++++ davisAPI/PyWeather-master/setup.py | 47 ++ davisAPI/PyWeather-master/weather/__init__.py | 18 + .../weather/services/__init__.py | 3 + .../weather/services/_base.py | 64 ++ .../PyWeather-master/weather/services/file.py | 86 +++ .../PyWeather-master/weather/services/pws.py | 91 +++ .../weather/services/tests/__init__.py | 0 .../weather/services/tests/test_file.py | 60 ++ .../weather/services/wunderground.py | 121 +++ .../weather/stations/__init__.py | 1 + .../weather/stations/_struct.py | 50 ++ .../weather/stations/davis.py | 702 ++++++++++++++++++ .../weather/stations/netatmo.py | 74 ++ .../weather/stations/station.py | 104 +++ .../weather/stations/tests/__init__.py | 0 .../weather/stations/tests/test_davis.py | 116 +++ .../weather/stations/tests/test_station.py | 17 + .../weather/stations/validate.py | 28 + .../weather/units/__init__.py | 4 + .../PyWeather-master/weather/units/astro.py | 103 +++ .../PyWeather-master/weather/units/precip.py | 21 + .../weather/units/pressure.py | 209 ++++++ .../PyWeather-master/weather/units/temp.py | 158 ++++ .../weather/units/tests/__init__.py | 0 .../weather/units/tests/test_pressure.py | 302 ++++++++ .../weather/units/tests/test_temp.py | 212 ++++++ .../weather/units/tests/test_wind.py | 215 ++++++ .../PyWeather-master/weather/units/wind.py | 123 +++ davisAPI/davisAPI.py | 77 ++ davisAPI/davisAPI.py.save | 69 ++ davisAPI/davisAPI.py.save.1 | 69 ++ davisAPI/davisAPI.py.save.2 | 82 ++ davisAPI/get_ports.py | 15 + davisAPI/get_ports.py.save | 15 + davisAPI/runme.sh | 3 + davisAPI/runner.py | 9 + davisAPI/ulstu_influx.py | 27 + 47 files changed, 4467 insertions(+) create mode 100644 davisAPI/.vscode/settings.json create mode 100644 davisAPI/PyWeather-master/.gitignore create mode 100644 davisAPI/PyWeather-master/CHANGELOG.txt create mode 100644 davisAPI/PyWeather-master/COPYING.txt create mode 100644 davisAPI/PyWeather-master/INSTALL.txt create mode 100644 davisAPI/PyWeather-master/MANIFEST.in create mode 100644 davisAPI/PyWeather-master/Makefile create mode 100644 davisAPI/PyWeather-master/README.md create mode 100644 davisAPI/PyWeather-master/scripts/weatherpub.conf.example create mode 100644 davisAPI/PyWeather-master/scripts/weatherpub.py create mode 100644 davisAPI/PyWeather-master/setup.py create mode 100644 davisAPI/PyWeather-master/weather/__init__.py create mode 100644 davisAPI/PyWeather-master/weather/services/__init__.py create mode 100644 davisAPI/PyWeather-master/weather/services/_base.py create mode 100644 davisAPI/PyWeather-master/weather/services/file.py create mode 100644 davisAPI/PyWeather-master/weather/services/pws.py create mode 100644 davisAPI/PyWeather-master/weather/services/tests/__init__.py create mode 100644 davisAPI/PyWeather-master/weather/services/tests/test_file.py create mode 100644 davisAPI/PyWeather-master/weather/services/wunderground.py create mode 100644 davisAPI/PyWeather-master/weather/stations/__init__.py create mode 100644 davisAPI/PyWeather-master/weather/stations/_struct.py create mode 100644 davisAPI/PyWeather-master/weather/stations/davis.py create mode 100644 davisAPI/PyWeather-master/weather/stations/netatmo.py create mode 100644 davisAPI/PyWeather-master/weather/stations/station.py create mode 100644 davisAPI/PyWeather-master/weather/stations/tests/__init__.py create mode 100644 davisAPI/PyWeather-master/weather/stations/tests/test_davis.py create mode 100644 davisAPI/PyWeather-master/weather/stations/tests/test_station.py create mode 100644 davisAPI/PyWeather-master/weather/stations/validate.py create mode 100644 davisAPI/PyWeather-master/weather/units/__init__.py create mode 100644 davisAPI/PyWeather-master/weather/units/astro.py create mode 100644 davisAPI/PyWeather-master/weather/units/precip.py create mode 100644 davisAPI/PyWeather-master/weather/units/pressure.py create mode 100644 davisAPI/PyWeather-master/weather/units/temp.py create mode 100644 davisAPI/PyWeather-master/weather/units/tests/__init__.py create mode 100644 davisAPI/PyWeather-master/weather/units/tests/test_pressure.py create mode 100644 davisAPI/PyWeather-master/weather/units/tests/test_temp.py create mode 100644 davisAPI/PyWeather-master/weather/units/tests/test_wind.py create mode 100644 davisAPI/PyWeather-master/weather/units/wind.py create mode 100644 davisAPI/davisAPI.py create mode 100644 davisAPI/davisAPI.py.save create mode 100644 davisAPI/davisAPI.py.save.1 create mode 100644 davisAPI/davisAPI.py.save.2 create mode 100644 davisAPI/get_ports.py create mode 100644 davisAPI/get_ports.py.save create mode 100644 davisAPI/runme.sh create mode 100644 davisAPI/runner.py create mode 100644 davisAPI/ulstu_influx.py diff --git a/davisAPI/.vscode/settings.json b/davisAPI/.vscode/settings.json new file mode 100644 index 0000000..7ed9aa7 --- /dev/null +++ b/davisAPI/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.analysis.extraPaths": [ + "./PyWeather-master" + ] +} \ No newline at end of file diff --git a/davisAPI/PyWeather-master/.gitignore b/davisAPI/PyWeather-master/.gitignore new file mode 100644 index 0000000..b054128 --- /dev/null +++ b/davisAPI/PyWeather-master/.gitignore @@ -0,0 +1,5 @@ +*.pyc +/build +.idea/ +scripts/weatherpub.conf +weather.egg-info/ \ No newline at end of file diff --git a/davisAPI/PyWeather-master/CHANGELOG.txt b/davisAPI/PyWeather-master/CHANGELOG.txt new file mode 100644 index 0000000..a6f1d85 --- /dev/null +++ b/davisAPI/PyWeather-master/CHANGELOG.txt @@ -0,0 +1,67 @@ +.. PyWeather // (c) 2010, Patrick C. McGinty + pyweather[@]tuxcoder[dot]com + +v0.11.0 +====== +:Release Date: 2021-04-03 +* Added support of the Netatmo Weather Station. +* Major update of weatherpub script: +* Supports Netatmo weather station in addition to Vantage Pro. +* Supports reading settings from the configuration file. +* The major update attempts to be backwards-compatible, although it is very + likely something is still broken. + +v0.10.0 +====== +:Release Date: 2020-11-09 +* Python 3 support + +v0.9.1 +====== +:Release Date: 8/9/10 +* Enhance debug output for publication failures + +v0.9 +====== +:Release Date: 7/16/10 + +* Renamed vpro-to-wu.py script to weatherpub.py +* Add support for pwsweather.com publication +* Add support for local file publication + +v0.8.2 +====== +:Release Date: 7/1/10 + +* Fixed run-time errors in vpro-to-wu.py script + +v0.8.1 +====== +:Release Date: 7/1/10 + +* Add id/password params to vpro-to-wu.py script +* Add update delay param to vpro-to-wu.py script +* Add TTY device selector to vpro-to-wu.py script + +v0.8 +======== +:Release Date: 6/25/10 + +* Add support for VantagePro CRC +* Add support for VantagePro DMPAFT command +* Misc fixes for VantagePro class +* Misc fixes for Wunderground Publisher class +* Add vpro-to-wu.py script for uploading Davis Vantage Pro data to + wunderground.com + +v0.7.1 +======== +:Release Date: 6/24/10 + +* Repackaged and updated build scripts. + +v0.7.0 +======== +:Release Date: 6/20/10 + +* Official release from Christopher Blunk. diff --git a/davisAPI/PyWeather-master/COPYING.txt b/davisAPI/PyWeather-master/COPYING.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/davisAPI/PyWeather-master/COPYING.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/davisAPI/PyWeather-master/INSTALL.txt b/davisAPI/PyWeather-master/INSTALL.txt new file mode 100644 index 0000000..ccebb53 --- /dev/null +++ b/davisAPI/PyWeather-master/INSTALL.txt @@ -0,0 +1,15 @@ +Any one of the following command will install PyWeather. Make sure to run as + ``root`` user: + + a. Use :command:`easy_install` from the `setuptools package + `_:: + + sudo easy_install weather + + b. Download the source distribution file and install from the + command line:: + + tar xzf weather-*.tar.gz + cd weather-* + sudo make install + diff --git a/davisAPI/PyWeather-master/MANIFEST.in b/davisAPI/PyWeather-master/MANIFEST.in new file mode 100644 index 0000000..3d6852f --- /dev/null +++ b/davisAPI/PyWeather-master/MANIFEST.in @@ -0,0 +1,3 @@ +include *.txt +include Makefile +include *.md \ No newline at end of file diff --git a/davisAPI/PyWeather-master/Makefile b/davisAPI/PyWeather-master/Makefile new file mode 100644 index 0000000..02aaf28 --- /dev/null +++ b/davisAPI/PyWeather-master/Makefile @@ -0,0 +1,55 @@ +# Makefile for installing and testing PyWeather +# +# Author: Patrick C. McGinty +# Date: Thursday, June 10 2010 + + +NAME=weather +VER=$(shell python -c 'import weather;print weather.__version__') +DIST_DIR=dist +TAR=${DIST_DIR}/${NAME}-${VER}.tar.gz +SRC_DIR=${DIST_DIR}/${NAME}-${VER} + +.PHONY: help +help: + @echo "The following targets are defined:" + @echo "" + @echo " clean remove tmp files" + @echo " dist create distribution archive file" + @echo " help print usage instructions for the Makefile" + @echo " install install program into system dirs" + @echo " test execute all unit tests" + @echo " release perform a full test/dist/install" + @echo " register update the PyPI registration" + @echo "" + +.PHONY: test +test: + nosetests + +.PHONY: install +install: + python setup.py install + +.PHONY: clean +clean: + python setup.py clean + +.PHONY: dist +dist: + python setup.py sdist --force-manifest + make clean + +.PHONY: dist-test +dist-test: + tar xzf ${TAR} -C ${DIST_DIR} + sudo make -C ${SRC_DIR} install + sudo rm -rf ${SRC_DIR} + +.PHONY: release +release: test dist dist-test + +.PHONY: register +register: + python setup.py register + diff --git a/davisAPI/PyWeather-master/README.md b/davisAPI/PyWeather-master/README.md new file mode 100644 index 0000000..4f6b433 --- /dev/null +++ b/davisAPI/PyWeather-master/README.md @@ -0,0 +1,85 @@ +# PyWeather + +## Abstract + +PyWeather contains weather related modules implemented in Python. +Anything weather related is fair game for PyWeather. Currently +PyWeather is limited to unit conversion, console reading, and data +publication. But, future work can be added to PyWeather in any area. + + +## Unit Conversion + +PyWeather has a lot of support for common unit conversions in +distance, temperature, pressure, and volume. Conversion from +Fahrenheit to Celsius, and kelvin is supported, as well as conversions +between inches of mercury and millibars. + + +## Station Observations + +PyWeather also contains modules that are capable of downloading +observations from weather consoles. The current list of supported +weather consoles includes: + +- Davis Vantage Pro +- Davis Vantage Pro2 +- Netatmo weather station. + + +## Data Publication + +PyWeather contains a module that allows developers to post conditions +to weather aggregation sites. The current list of support services includes: + +- WeatherUnderground (wundgerground.com) +- PWS Weather (pwsweather.com) +- WeatherForYou (weatherforyou.com) + + +For additional information, please email the maintainer: + pyweather@tuxcoder.com + +## Data Publication Script + +`scripts/weatherpub.py` supports publication of the weather data. It can also serve as a good usage example. + +### General usage + +1. Copy `weatherpub.conf.example` as `weatehrpub.conf`. +2. Modify `weatehrpub.conf`: +3. In `[general]` section set `station` to the name of the station you have. +4. Set `publication` to a comma-separated list of weather services you'd like + to push data to. +5. Configure weather station and publication service in corresponding sections + of the configuration file (see below for more details). +3. Run it: `./scripts/weatherpub.py -c scripts/weatherpub.conf` + +### Weather stations + +The script supports Vantage Pro and Netatmo. Vantage Pro support was not recently tested and may be broken by the latest update. Please report bugs and/or send pull requests. + +#### Vantage Pro + +By default script excepts Vantage Pro weather stations to be connected to /`/dev/ttyS0`. Use `--tty` command-line flag to override it (this cannot be currently set via command-line), e.g. `./scripts/weatherpub.py -c scripts/weatherpub.conf --tty /dev/ttyS1` + +#### Netatmo + +Since netatmo works via public API, some setup required first: + +1. [Create a Netatmo app](https://dev.netatmo.com/apps/createanapp#form). +2. Use generated app id and secret as `client_id` and `client_secret`. +3. Use your own Netatmo username (e-mail) as `username` and `password`. +4. Set `module_name` to your outdoor module name, e.g. 'Outdoor'. + +Note: Rain Gauge and Anemometer are not supported yet. + +### Publication services + +Only 3 publication service are currently supported. Out of them only PWS Weather was properly tested. + +#### PWS Weather + +1. [Create PWS Weather profile](https://www.pwsweather.com/register). +2. Go to your [dashboard](https://dashboard.pwsweather.com/) and [create a station](https://dashboard.pwsweather.com/stations/add). +3. In `[pwsweather]` section of the configuration file use your station id as `site_id`, and password from account you created at step 1 as `password`. diff --git a/davisAPI/PyWeather-master/scripts/weatherpub.conf.example b/davisAPI/PyWeather-master/scripts/weatherpub.conf.example new file mode 100644 index 0000000..6cb1d49 --- /dev/null +++ b/davisAPI/PyWeather-master/scripts/weatherpub.conf.example @@ -0,0 +1,23 @@ +[general] +# Weather station name. Currently supported: vantage_pro, netatmo. +station=netatmo +# Publication websites, comma-separated. +# Currently supported: 'wug', 'pwsweather', 'file' +publication=pwsweather + +## +# Netatmo weather station settings +[netatmo] + +client_id="" +client_secret="" +username="" +password="" +module_name=Outdoor + +## +# pwsweather.com settings +[pwsweather] + +site_id=MySiteID +password=MyPassowrd diff --git a/davisAPI/PyWeather-master/scripts/weatherpub.py b/davisAPI/PyWeather-master/scripts/weatherpub.py new file mode 100644 index 0000000..dac2ff7 --- /dev/null +++ b/davisAPI/PyWeather-master/scripts/weatherpub.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# +# PyWeather example script for reading PyWeather stations uploading to one or +# more PyWeather publication sites +# +# Author: Patrick C. McGinty +# Email: pyweather@tuxcoder.com +# Date: Sunday, May 02 2010 +''' +Periodically read data from a local weather station and upload to the PyWeather +publication site. +''' + +import os +import sys +import time +import logging +import optparse +import configparser + +import weather.stations +import weather.stations.netatmo +import weather.services + +log = logging.getLogger('') + +# Intervals (in minutes) between each archive record generated by the weather +# station: +ARCHIVE_INTERVAL = 10 +# Gust 'time to live'; define many minutes should gust be reported: +GUST_TTL = 10 +GUST_MPH_MIN = 7 # minimum mph of gust above avg wind speed to report + +# Publication Services Lookup Table +# key expected to match optparse destination parameter +# value defines class object of publication service +PUB_SERVICES = { + 'wug': weather.services.Wunderground, + 'pws': weather.services.PwsWeather, + 'pwsweather': weather.services.PwsWeather, + 'file': weather.services.TextFile, +} + +STATIONS = { + 'netatmo': weather.stations.netatmo.NetatmoStation, + 'vantage_pro': weather.stations.VantagePro, +} + + +class NoSensorException(Exception): + pass + + +class WindGust(object): + NO_VALUE = ('NA', 'NA') + + def __init__(self): + self.value = self.NO_VALUE + self.count = 0 + + def get(self, station, interval): + ''' + return gust data, if above threshold value and current time is inside + reporting window period + ''' + rec = station.fields['Archive'] + # process new data + if rec: + threshold = station.fields['WindSpeed10Min'] + GUST_MPH_MIN + if rec['WindHi'] >= threshold: + self.value = (rec['WindHi'], rec['WindHiDir']) + self.count = GUST_TTL * 60 / interval + else: + self.value = self.NO_VALUE + + # return gust value, if remaining time is left, and valid + if self.count: + self.count -= 1 + else: + self.value = self.NO_VALUE + + log.debug('wind gust of {0} mph from {1}'.format(*self.value)) + return self.value + + +WindGust = WindGust() + + +def weather_update(station, pub_sites, interval): + ''' + main execution loop. query weather data and post to online service. + ''' + point = station.get_reading() + + # santity check weather data + if point.temperature_f > 200: + raise NoSensorException( + 'Out of range temperature value: %.1f, check sensors' % + (point.temperature_f,)) + + gust = None + gust_dir = None + if isinstance(station, weather.stations.VantagePro): + # Wind is only supported in VantagePro. + gust, gust_dir = WindGust.get(station, interval) + + # upload data in the following order: + for ps in pub_sites: + try: # try block necessary to attempt every publisher + ps.set( + pressure=point.pressure, + dewpoint=point.dew_point_f, + humidity=point.humidity, + tempf=point.temperature_f, + rainin=point.rain_rate_in, + rainday=point.rain_day_in, + windspeed=point.wind_speed_mph, + winddir=point.wind_direction, + windgust=gust, + windgustdir=gust_dir, + dateutc=point.time.strftime("%Y-%m-%d %H:%M:%S")) + ps.publish() + # TODO: add user-friendly name + log.info("Published to %s", ps.__class__) + except (Exception) as e: + log.exception('publisher %s: %s' % (ps.__class__.__name__, e)) + + +def init_log(quiet, debug): + ''' + setup system logging to desired verbosity. + ''' + from logging.handlers import SysLogHandler + fmt = logging.Formatter( + os.path.basename(sys.argv[0]) + + ".%(name)s %(levelname)s - %(message)s") + facility = SysLogHandler.LOG_DAEMON + syslog = SysLogHandler(address='/dev/log', facility=facility) + syslog.setFormatter(fmt) + log.addHandler(syslog) + if not quiet: + console = logging.StreamHandler() + console.setFormatter(fmt) + log.addHandler(console) + log.setLevel(logging.INFO) + if debug: + log.setLevel(logging.DEBUG) + + +def get_pub_services(opts, config): + ''' + use values in opts data to generate instances of publication services. + ''' + sites = [] + for p_key in list(vars(opts).keys()): + args = getattr(opts, p_key) + if p_key in PUB_SERVICES and args: + if isinstance(args, tuple): + ps = PUB_SERVICES[p_key](*args) + else: + ps = PUB_SERVICES[p_key](args) + sites.append(ps) + if config: + for p_key in config['general']['publication'].split(','): + ps = PUB_SERVICES[p_key](**config[p_key]) + sites.append(ps) + return sites + + +def get_options(parser): + ''' + read command line options to configure program behavior. + ''' + # station services + # publication services + pub_g = optparse.OptionGroup( + parser, "Publication Services", + 'One or more publication service must be specified to enable upload ' + 'of weather data.',) + pub_g.add_option( + '-w', '--wundergound', nargs=2, type='string', dest='wug', + help='Weather Underground service; WUG=[SID(station ID), PASSWORD]') + pub_g.add_option( + '-p', '--pws', nargs=2, type='string', dest='pws', + help='PWS service; PWS=[SID(station ID), PASSWORD]') + pub_g.add_option( + '-f', '--file', nargs=1, type='string', dest='file', + help='Local file; FILE=[FILE_NAME]') + parser.add_option_group(pub_g) + + parser.add_option( + '-d', '--debug', dest='debug', action="store_true", + default=False, help='enable verbose debug logging') + parser.add_option( + '-q', '--quiet', dest='quiet', action="store_true", + default=False, help='disable all console logging') + parser.add_option( + '-t', '--tty', dest='tty', default='/dev/ttyS0', + help='set serial port device [/dev/ttyS0]') + parser.add_option( + '-n', '--interval', dest='interval', default=60, + type='int', help='polling/update interval in seconds [60]') + parser.add_option('-c', '--config', dest='config_path', default=None, + type='str', help='path to the configuration file') + return parser.parse_args() + + +if __name__ == '__main__': + parser = optparse.OptionParser() + opts, args = get_options(parser) + init_log(opts.quiet, opts.debug) + config = None + + if opts.config_path: + config = configparser.ConfigParser() + config.read_file(open(opts.config_path)) + + # configure publication service defined in command-line args + pub_sites = get_pub_services(opts, config) + + if not pub_sites: + log.error('no publication service defined') + sys.exit(-1) + + if config: + station_name = config['general']['station'] + station = STATIONS[station_name](**config[station_name]) + else: + # Only VantagePro is supported without config. + station = weather.stations.VantagePro(opts.tty, ARCHIVE_INTERVAL) + + while True: + try: + weather_update(station, pub_sites, opts.interval) + except (Exception) as e: + log.exception(e) + # pause until next update time + next_update = opts.interval - (time.time() % opts.interval) + log.info('sleep') + time.sleep(next_update) diff --git a/davisAPI/PyWeather-master/setup.py b/davisAPI/PyWeather-master/setup.py new file mode 100644 index 0000000..cb248bd --- /dev/null +++ b/davisAPI/PyWeather-master/setup.py @@ -0,0 +1,47 @@ +#! /usr/bin/env python +# +# PyWeather +# (c) 2010 Patrick C. McGinty +# (c) 2005 Christopher Blunck +# +# You're welcome to redistribute this software under the +# terms of the GNU General Public Licence version 2.0 +# or, at your option, any higher version. +# +# You can read the complete GNU GPL in the file COPYING +# which should come along with this software, or visit +# the Free Software Foundation's WEB site http://www.fsf.org +# + +import os +from distutils.core import setup +from pathlib import Path + +import weather as pkg +name = pkg.__name__ + +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() + +setup(name=name, + version=pkg.__version__, + license="GNU GPL", + description=pkg.__doc__, + long_description = long_description, + long_description_content_type="text/markdown", + author="Patrick C. McGinty, Christopher Blunck", + author_email="pyweather@tuxcoder.com, chris@wxnet.org", + url="http://github.com/cmcginty/PyWeather", + download_url="https://github.com/cmcginty/PyWeather/archive/%s.zip" % + pkg.__version__, + packages=[ + name, + name + '.services', + name + '.stations', + name + '.units', + ], + install_requires=[ + 'pyserial==3.5' + ], + scripts=['scripts/weatherpub.py'], + ) diff --git a/davisAPI/PyWeather-master/weather/__init__.py b/davisAPI/PyWeather-master/weather/__init__.py new file mode 100644 index 0000000..ddad823 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/__init__.py @@ -0,0 +1,18 @@ +''' +PyWeather branch; bindings for Davis Vantage Pro and Pro2 weather stations, +upload of weather data (e.g. wunderground.com), and meteorological +calculation/conversion functions. +''' + +__version__ = '0.11.0' + +import logging + + +class NullHandler(logging.Handler): + def emit(self, record): + pass + + +# init a null handler, to prevent warnings +logging.getLogger(__name__).addHandler(NullHandler()) diff --git a/davisAPI/PyWeather-master/weather/services/__init__.py b/davisAPI/PyWeather-master/weather/services/__init__.py new file mode 100644 index 0000000..1fc1320 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/services/__init__.py @@ -0,0 +1,3 @@ +from .wunderground import * +from .pws import * +from .file import * diff --git a/davisAPI/PyWeather-master/weather/services/_base.py b/davisAPI/PyWeather-master/weather/services/_base.py new file mode 100644 index 0000000..7d7e29e --- /dev/null +++ b/davisAPI/PyWeather-master/weather/services/_base.py @@ -0,0 +1,64 @@ +''' +Common base classes for PyWeather pulication services. +''' + + +import logging +log = logging.getLogger(__name__) + + +class PublishException(Exception): + pass + + +class HttpPublisher(object): + ''' + Abstract base class for creation generic HTTP publication services + ''' + SOFTWARE = 'PyWeather' + STD_SERVER = None + REALTIME_SERVER = None + URI = None + + def __init__(self, sid, password, rtfreq=None): + self.sid = sid + self.password = password + self.rtfreq = rtfreq + + def set(self, *args, **kw): + ''' + Useful for defining weather data published to the server. Each + publication service implements their own supported keyword args, but + should support any number of arguments. + ''' + raise NotImplementedError("abstract method") + + @staticmethod + def _publish(args, server, uri): + from http.client import HTTPConnection, HTTPSConnection + from urllib.parse import urlencode + + args = {k: v for k, v in args.items() if v is not None and v != 'NA'} + uri = uri + "?" + urlencode(args) + + log.debug('Connect to: https://%s' % server) + log.debug('GET %s' % uri) + + conn = HTTPSConnection(server, timeout=5) + if not conn: + raise PublishException('Remote server connection timeout') + conn.request("GET", uri) + + http = conn.getresponse() + data = (http.status, http.reason, http.read()) + conn.close() + if not (data[0] == 200 and data[1] == 'OK'): + raise PublishException( + 'Server returned invalid status: %d %s %s' % data) + return data + + def publish(self): + ''' + Perform HTTP session to transmit defined weather values. + ''' + return self._publish(self.args, self.server, self.URI) diff --git a/davisAPI/PyWeather-master/weather/services/file.py b/davisAPI/PyWeather-master/weather/services/file.py new file mode 100644 index 0000000..4808e93 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/services/file.py @@ -0,0 +1,86 @@ +''' +Local File Publisher + +Abstract: +The class contained within this module allows python programs to +publish weather conditions to a local text file. The format of the file is: + + field value [value ...] + field value [value ...] + field value [value ...] + ... + ... + +Each 'field' will begin on a separate line. The 'field' parameter is always a +single word. Depending on the field, there maybe be multiple 'value' +parameters. All fields and values are separated by a single space. String +values will be surrounded by quotes. + +This class does not define field names. The implementation assigns field names +from the keyword parameters passed to it through the set() method. Therefore it +is up to the user to define all field names using named parameters with the +'set()' method. If you desire to keep the TextFile.set() command compatible +with other set() publisher methods, please reference the other classes for +expected field names. + +Usage: +>>> publisher = TextFile( 'file_name' ) +>>> publisher.set( ... ) +>>> publisher.publish() + +Author: Patrick C. McGinty (pyweather@tuxcoder.com) +Date: Thursday, July 15 2010 +''' + + + +import io +import logging +log = logging.getLogger(__name__) + +from . _base import * + + +class TextFile(object): + ''' + Publishes weather data to a local file. See module + documentation for additional information and usage idioms. + ''' + def __init__(self, file_name): + self.file_name = file_name + self.args = {} + + def set( self, **kw): + ''' + Store keyword args to be written to output file. + ''' + self.args = kw + log.debug( self.args ) + + + @staticmethod + def _append_vals( buf, val): + if isinstance(val,dict): + msg = 'unsupported %s type: %s' % (type(val),repr(val),) + log.error(msg) + if isinstance(val,(list,tuple)): + for i in val: + TextFile._append_vals(buf, i) + else: + buf.write(' ' + repr(val)) + + + def publish(self): + ''' + Write output file. + ''' + with open( self.file_name, 'w') as fh: + for k,v in self.args.items(): + buf = io.StringIO() + buf.write(k) + self._append_vals(buf,v) + fh.write(buf.getvalue() + '\n') + buf.close() # free string buffer + + + diff --git a/davisAPI/PyWeather-master/weather/services/pws.py b/davisAPI/PyWeather-master/weather/services/pws.py new file mode 100644 index 0000000..a760f16 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/services/pws.py @@ -0,0 +1,91 @@ +''' +PWSweather.com Publisher +WeatherForYou.com Publisher + +Abstract: +The class contained within this module allows python programs to +publish weather conditions to the pwsweather.com servers. + +Usage: +>>> publisher = PwsWeather( 'MySiteID', 'MyPassowrd') +>>> publisher.set( ... ) +>>> response = publisher.publish() +>>> print '%s: %s' % (response.status, response.reason) + +Notes on arguments to Publisher.set(): + pressure: in inches of Hg + dewpoint: in Fahrenheit + humidity: between 0.0 and 100.0 inclusive + tempf: in Fahrenheit + rainin: inches/hour of rain + rainday: total rainfall for day (localtime) + rainmonth: total rainfall for month (localtime) + rainyear: total rainfall for year (localtime) + dateutc: date string, "YYYY-MM-DD HH:MM:SS" + windgust: in mph + windspeed: in mph + winddir: in degrees, between 0.0 and 360.0 + weather: unknown at this time (email me if you know!) + +Author: Patrick C. McGinty (pyweather@tuxcoder.com) +Date: Tuesday, July 13 2010 +''' + +import logging +from . _base import * + +log = logging.getLogger(__name__) + + +class PwsWeather(HttpPublisher): + ''' + Publishes weather data to the pwsweather.com servers. See module + documentation for additional information and usage idioms. + ''' + STD_SERVER = "www.pwsweather.com" + URI = "/pwsupdate/pwsupdate.php" + + def __init__(self, sid: str = None, password: str = None, + site_id: str = None): + super(PwsWeather, self).__init__(sid, password) + self.args = {'ID': sid or site_id, + 'PASSWORD': password, + 'action': 'updateraw', + 'softwaretype': self.SOFTWARE} + self.server = self.STD_SERVER + + def set(self, pressure='NA', dewpoint='NA', humidity='NA', tempf='NA', + rainin='NA', rainday='NA', rainmonth='NA', rainyear='NA', + dateutc='NA', windgust='NA', windspeed='NA', winddir='NA', + weather='NA', *args, **kw): + ''' + Define weatehr data published to the server. + + Parameters not sent will be cleared and not set to server. Unknown + keyword args will be silently ignored, so be careful. This is necessary + for publishers that support more fields than others. + ''' + # unused, but valid, parameters are: + # solarradiation, UV + self.args.update({ + 'baromin': pressure, + 'dailyrainin': rainday, + 'dateutc': dateutc, + 'dewptf': dewpoint, + 'humidity': humidity, + 'monthrainin': rainmonth, + 'rainin': rainin, + 'tempf': tempf, + 'weather': weather, + 'winddir': winddir, + 'windgustmph': windgust, + 'windspeedmph': windspeed, + 'yearrainin': rainyear}) + log.debug(self.args) + + def publish(self): + http = super(PwsWeather, self).publish() + if not http[2].find(b'Logged and posted') >= 0: + raise PublishException( + 'Server returned invalid status: %d %s %s' % http) + return http diff --git a/davisAPI/PyWeather-master/weather/services/tests/__init__.py b/davisAPI/PyWeather-master/weather/services/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/davisAPI/PyWeather-master/weather/services/tests/test_file.py b/davisAPI/PyWeather-master/weather/services/tests/test_file.py new file mode 100644 index 0000000..ac70e67 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/services/tests/test_file.py @@ -0,0 +1,60 @@ + +import builtins +import unittest +from mock import Mock, patch, mock_open + +from ..file import TextFile + + +class TestPublish(unittest.TestCase): + + F = TextFile('output.txt') + + def test_set(self): + self.F.set(a=1, b=2, c=3) + self.assertEqual(self.F.args, {'a': 1, 'b': 2, 'c': 3}) + self.F.set(c=1, d=2, e=3) + self.assertEqual(self.F.args, {'c': 1, 'd': 2, 'e': 3}) + + def test_positional_set(self): + self.assertRaises(TypeError, self.F.set, 1, 2, 3) + + def test_single_value(self): + m = mock_open() + with patch.object(builtins, 'open', m): + self.F.set(a=1) + self.F.publish() + + m.assert_called() + handle = m() + handle.write.assert_called_with('a 1\n') + + def test_string_value(self): + m = mock_open() + with patch.object(builtins, 'open', m): + self.F.set(a='string arg') + self.F.publish() + + m.assert_called() + handle = m() + handle.write.assert_called_with("a 'string arg'\n") + + def test_list_value(self): + m = mock_open() + with patch.object(builtins, 'open', m): + self.F.set(a=[1, 2, 3]) + self.F.publish() + + m.assert_called() + handle = m() + handle.write.assert_called_with("a 1 2 3\n") + + def test_double_list_value(self): + m = mock_open() + with patch.object(builtins, 'open', m): + self.F.set(a=[['a', 'b', 'c'], 2, 3]) + self.F.publish() + + m.assert_called() + handle = m() + handle.write.assert_called_with("a 'a' 'b' 'c' 2 3\n") diff --git a/davisAPI/PyWeather-master/weather/services/wunderground.py b/davisAPI/PyWeather-master/weather/services/wunderground.py new file mode 100644 index 0000000..2f5c8ec --- /dev/null +++ b/davisAPI/PyWeather-master/weather/services/wunderground.py @@ -0,0 +1,121 @@ +''' +WUnderground.com Publisher + +Abstract: +The class contained within this module allows python programs to +publish weather conditions to the wunderground.com servers. That is, +this class encapsulates the wire protocol wunderground.com supports +and allows application developers to insulate themselves against +changes in the wunderground.com wire protocol. + +If the rtfreq parameter is passed to the Publisher constructor, +posting of the current conditions will go to the "real time updater" +service provided by wunderground.com. The rtfreq optional parameter +passed to the constructor is a float that represents the number of +seconds between observations. + +Usage: +>>> publisher = Wunderground( 'MySiteID', 'MyPassowrd') +>>> publisher.set( ... ) +>>> response = publisher.publish() +>>> print '%s: %s' % (response.status, response.reason) + +Notes on arguments to Publisher.set(): + pressure: in inches of Hg + dewpoint: in Fahrenheit + humidity: between 0.0 and 100.0 inclusive + tempf: in Fahrenheit + rainin: inches/hour of rain + rainday: total rainfall in day (localtime) + dateutc: date "YYYY-MM-DD HH:MM:SS" in GMT timezone + windgust: in mph + windgustdir:in degrees, between 0.0 and 360.0 + windspeed: in mph + winddir: in degrees, between 0.0 and 360.0 + clouds: unknown at this time (email me if you know!) + weather: unknown at this time (email me if you know!) + +Developers Notes: +It appears that even if you provide an invalid username and password, +a status of 200, and a reason of "OK" is returned. + +Author: Patrick C. McGinty (pyweather@tuxcoder.com) +Date: Tuesday, July 13 2010 +Author: Christopher Blunck (chris@wxnet.org) +Date: 2006-03-27 +''' + + +import logging + +from . _base import * + +log = logging.getLogger(__name__) + + +class Wunderground(HttpPublisher): + ''' + Publishes weather data to the wunderground.com servers. See + module documentation for additional information and usage idioms. + ''' + STD_SERVER = "weatherstation.wunderground.com" + REALTIME_SERVER = "rtupdate.wunderground.com" + URI = "/weatherstation/updateweatherstation.php" + + def __init__(self, sid, password, rtfreq=None): + super(Wunderground, self).__init__(sid, password) + self.args = {'ID': sid, + 'PASSWORD': password, + 'action': 'updateraw', + 'softwaretype': self.SOFTWARE, } + if rtfreq: + self.args['realtime'] = 1 + self.args['rtfreq'] = self.rtfreq + self.server = self.REALTIME_SERVER + else: + self.server = self.STD_SERVER + + def set(self, pressure='NA', dewpoint='NA', humidity='NA', tempf='NA', + rainin='NA', rainday='NA', dateutc='NA', windgust='NA', + windgustdir='NA', windspeed='NA', winddir='NA', + clouds='NA', weather='NA', *args, **kw): + ''' + Useful for defining weather data published to the server. Parameters + not set will be reset and not sent to server. Unknown keyword args will + be silently ignored, so be careful. This is necessary for publishers + that support more fields than others. + ''' + # see: http://wiki.wunderground.com/index.php/PWS_-_Upload_Protocol + # unused, but valid, parameters are: + # windspdmph_avg2m, winddir_avg2m, windgustmph_10m, windgusdir_10m + # soiltempf, soilmoisture, leafwetness, solarradiation, UV + # indoortempf, indoorhumidity + self.args.update({ + 'baromin': pressure, + 'clouds': clouds, + 'dailyrainin': rainday, + 'dateutc': dateutc, + 'dewptf': dewpoint, + 'humidity': humidity, + 'rainin': rainin, + 'tempf': tempf, + 'weather': weather, + 'winddir': winddir, + 'windgustdir': windgustdir, + 'windgustmph': windgust, + 'windspeedmph': windspeed, + }) + log.debug(self.args) + + def publish(self): + http = super(Wunderground, self).publish() + if not http[2].find(b'success') >= 0: + raise PublishException( + 'Server returned invalid status: %d %s %s' % http) + return http + + +# for legacy support <= v0.8.2, depreciated, do not use +Publisher = Wunderground + +# vim: sts=4:ts=4:sw=4 diff --git a/davisAPI/PyWeather-master/weather/stations/__init__.py b/davisAPI/PyWeather-master/weather/stations/__init__.py new file mode 100644 index 0000000..f6b9b8f --- /dev/null +++ b/davisAPI/PyWeather-master/weather/stations/__init__.py @@ -0,0 +1 @@ +from .davis import * diff --git a/davisAPI/PyWeather-master/weather/stations/_struct.py b/davisAPI/PyWeather-master/weather/stations/_struct.py new file mode 100644 index 0000000..0a4a63d --- /dev/null +++ b/davisAPI/PyWeather-master/weather/stations/_struct.py @@ -0,0 +1,50 @@ +""" +Binary Data Interfaces + +Abstract: +Helper classes for working with binary data structures. This simplifies data +field extraction from a binary buffer. + +Author: Patrick C. McGinty (pyweather@tuxcoder.com) +Date: 2010-06-025 +""" + + +import struct + + +class Struct(struct.Struct): + """ + Implements a reusable class for working with a binary data structure. It + provides a named fields interface, similar to C structures. + + Usage: 1) subclass and extend _post_unpack method + 2) instantiate directly, if no 'post unpack' processing needed + + Arguments: + See `struct.Struct` class definition. + """ + def __init__(self, fmt, order='@'): + self.fields, fmt_t = list(zip(*fmt)) + super(Struct, self).__init__(order + ''.join(fmt_t)) + + def unpack(self, buf): + """ + see unpack_from() + """ + return self.unpack_from(buf, offset=0) + + def unpack_from(self, buf, offset=0): + """ + unpacks data from 'buf' and returns a dictation of named fields. the + fields can be post-processed by extending the _post_unpack() method. + """ + data = super(Struct, self).unpack_from(buf, offset) + items = dict(list(zip(self.fields, data))) + return self._post_unpack(items) + + def _post_unpack(self, items): + """ + perform data modification of any values, after unpacking from a buffer. + """ + return items diff --git a/davisAPI/PyWeather-master/weather/stations/davis.py b/davisAPI/PyWeather-master/weather/stations/davis.py new file mode 100644 index 0000000..4e36caa --- /dev/null +++ b/davisAPI/PyWeather-master/weather/stations/davis.py @@ -0,0 +1,702 @@ +""" +Davis Vantage Pro and Pro2 Service + +Abstract: +Allows data query of Davis Vantage Pro and Pro2 devices via serial port +interface. The primary implemented serial commands supported are LOOP and +DMPAFT. + +The LOOP command can acquire all real-time data points. The DMPAFT command is +used to acquire periodic high/low data. + +All data is returned in a dict structure with value/key pairs. Periodic data is +only captured once per period. When not active, the keys for periodic data are +not present in the results. + +Author: Patrick C. McGinty (pyweather@tuxcoder.com) +Date: 2010-06-025 + +Original Author: Christopher Blunck (chris@wxnet.org) +Date: 2006-03-27 +""" + +from ._struct import Struct +from ..units import * +from .station import * + +import logging +import serial +import struct +import time +from array import array +import datetime as dt + +log = logging.getLogger(__name__) + +# public interfaces for module +__all__ = ['VantagePro', 'NoDeviceException'] + +READ_DELAY = 5 +BAUD = 19200 + + +def log_raw(msg, raw): + log.debug(msg + ': ' + raw.decode()) + + +class NoDeviceException(Exception): + pass + + +class NoNewRecordsException(Exception): + pass + + +class VProCRC(object): + """ + Implements CRC algorithm, necessary for encoding and verifying data from + the Davis Vantage Pro unit. + """ + + CRC_TABLE = ( + 0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0, + ) + + @staticmethod + def get(data): + """ + return CRC calc value from raw serial data + """ + crc = 0 + for byte in array('B', data): + crc = (VProCRC.CRC_TABLE[(crc >> 8) ^ byte] ^ ((crc & 0xFF) << 8)) + return crc + + @staticmethod + def verify(data): + """ + perform CRC check on raw serial data, return true if valid. + a valid CRC == 0. + """ + if len(data) == 0: + return False + crc = VProCRC.get(data) + if crc: + log.info("CRC Bad") + else: + log.debug("CRC OK") + return not crc + + +# --------------------------------------------------------------------------- # + +class LoopStruct(Struct): + """ + For unpacking data structure returned by the 'LOOP' command. this structure + contains all the real-time data that can be read from the Davis Vantage Pro. + """ + FMT = ( + ('LOO', '3s'), ('BarTrend', 'B'), ('PacketType', 'B'), + ('NextRec', 'H'), ('Pressure', 'H'), ('TempIn', 'H'), + ('HumIn', 'B'), ('TempOut', 'H'), ('WindSpeed', 'B'), + ('WindSpeed10Min', 'B'), ('WindDir', 'H'), ('ExtraTemps', '7s'), + ('SoilTemps', '4s'), ('LeafTemps', '4s'), ('HumOut', 'B'), + ('HumExtra', '7s'), ('RainRate', 'H'), ('UV', 'B'), + ('SolarRad', 'H'), ('RainStorm', 'H'), ('StormStartDate', 'H'), + ('RainDay', 'H'), ('RainMonth', 'H'), ('RainYear', 'H'), + ('ETDay', 'H'), ('ETMonth', 'H'), ('ETYear', 'H'), + ('SoilMoist', '4s'), ('LeafWetness', '4s'), ('AlarmIn', 'B'), + ('AlarmRain', 'B'), ('AlarmOut', '2s'), ('AlarmExTempHum', '8s'), + ('AlarmSoilLeaf', '4s'), ('BatteryStatus', 'B'), ('BatteryVolts', 'H'), + ('ForecastIcon', 'B'), ('ForecastRuleNo', 'B'), ('SunRise', 'H'), + ('SunSet', 'H'), ('EOL', '2s'), ('CRC', 'H'), + ) + + def __init__(self): + super(LoopStruct, self).__init__(self.FMT, '=') + + def _post_unpack(self, items): + items['Pressure'] = items['Pressure'] / 1000.0 + items['TempIn'] = items['TempIn'] / 10.0 + items['TempOut'] = items['TempOut'] / 10.0 + items['RainRate'] = items['RainRate'] / 100.0 + items['RainStorm'] = items['RainStorm'] / 100.0 + items['StormStartDate'] = self._unpack_storm_date(items['StormStartDate']) + # rain totals + items['RainDay'] = items['RainDay'] / 100.0 + items['RainMonth'] = items['RainMonth'] / 100.0 + items['RainYear'] = items['RainYear'] / 100.0 + # evapotranspiration totals + items['ETDay'] = items['ETDay'] / 1000.0 + items['ETMonth'] = items['ETMonth'] / 100.0 + items['ETYear'] = items['ETYear'] / 100.0 + # soil moisture + leaf wetness + items['SoilMoist'] = struct.unpack('4B', items['SoilMoist']) + items['LeafWetness'] = struct.unpack('4B', items['LeafWetness']) + # battery statistics + items['BatteryVolts'] = items['BatteryVolts'] * 300 / 512.0 / 100.0 + # sunrise / sunset + items['SunRise'] = self._unpack_time(items['SunRise']) + items['SunSet'] = self._unpack_time(items['SunSet']) + return items + + @staticmethod + def _unpack_time(val): + """ + given a packed time field, unpack and return "HH:MM" string. + """ + # format: HHMM, and space padded on the left.ex: "601" is 6:01 AM + return "%02d:%02d" % divmod(val, 100) # covert to "06:01" + + @staticmethod + def _unpack_storm_date(date): + """ + given a packed storm date field, unpack and return 'YYYY-MM-DD' string. + """ + year = (date & 0x7f) + 2000 # 7 bits + day = (date >> 7) & 0x01f # 5 bits + month = (date >> 12) & 0x0f # 4 bits + return "%s-%s-%s" % (year, month, day) + + +# --------------------------------------------------------------------------- # + +class _ArchiveStruct(object): + """ + common features for both Rev.A and Rev.B structures. + """ + FMT = None + + def __init__(self): + super(_ArchiveStruct, self).__init__(self.FMT, '=') + + def _post_unpack(self, items): + vals = self._unpack_date_time(items['DateStamp'], items['TimeStamp']) + items.update(zip(('Year', 'Month', 'Day', 'Hour', 'Min'), vals)) + items['TempOut'] = items['TempOut'] / 10.0 + items['TempOutHi'] = items['TempOutHi'] / 10.0 + items['TempOutLow'] = items['TempOutLow'] / 10.0 + items['Barometer'] = items['Barometer'] / 1000.0 + items['TempIn'] = items['TempIn'] / 10.0 + items['UV'] = items['UV'] / 10.0 + items['UVHi'] = items['UVHi'] / 10.0 + items['ETHour'] = items['ETHour'] / 1000.0 + items['SoilTemps'] = tuple( + t - 90 for t in struct.unpack('4B', items['SoilTemps'])) + items['ExtraHum'] = struct.unpack('2B', items['ExtraHum']) + items['SoilMoist'] = struct.unpack('4B', items['SoilMoist']) + return items + + @staticmethod + def _unpack_date_time(date, time_): + day = date & 0x1f # 5 bits + month = (date >> 5) & 0x0f # 4 bits + year = ((date >> 9) & 0x7f) + 2000 # 7 bits + hour, min_ = divmod(time_, 100) + return year, month, day, hour, min_ + + +# --------------------------------------------------------------------------- # + +class _ArchiveAStruct(_ArchiveStruct, Struct): + FMT = ( + ('DateStamp', 'H'), ('TimeStamp', 'H'), ('TempOut', 'H'), + ('TempOutHi', 'H'), ('TempOutLow', 'H'), ('RainRate', 'H'), + ('RainRateHi', 'H'), ('Pressure', 'H'), ('SolarRad', 'H'), + ('WindSamps', 'H'), ('TempIn', 'H'), ('HumIn', 'B'), + ('HumOut', 'B'), ('WindAvg', 'B'), ('WindHi', 'B'), + ('WindHiDir', 'B'), ('WindAvgDir', 'B'), ('UV', 'B'), + ('ETHour', 'B'), ('unused', 'B'), ('SoilMoist', '4s'), + ('SoilTemps', '4s'), ('LeafWetness', '4s'), ('ExtraTemps', '2s'), + ('ExtraHum', '2s'), ('ReedClosed', 'H'), ('ReedOpened', 'H'), + ('unused', 'B'), + ) + + def _post_unpack(self, items): + items = super(_ArchiveAStruct, self)._post_unpack(items) + items['LeafWetness'] = struct.unpack('4B', items['LeafWetness']) + items['ExtraTemps'] = tuple( + t - 90 for t in struct.unpack('2B', items['ExtraTemps'])) + return items + + +# --------------------------------------------------------------------------- # + +class _ArchiveBStruct(_ArchiveStruct, Struct): + """ + This represents the structure of the Archive Packet (RevB) returned by the station with the DMPAFT command + """ + FMT = ( + # These 16 bits hold the date that the archive was written in the following format: + # Year (7 bits) | Month (4 bits) | Day (5 bits) or: day + month*32 + (year-2000)*512) + ('DateStamp', 'H'), + # Time on the Vantage that the archive record was + # written: + # (Hour * 100) + minute. + ('TimeStamp', 'H'), + # Either the Average Outside Temperature, or the + # Final Outside Temperature over the archive period. + # Units are (F / 10) + ('TempOut', 'H'), + # Highest Outside Temp over the archive period. + ('TempOutHi', 'H'), + # Lowest Outside Temp over the archive period. + ('TempOutLow', 'H'), + # Number of rain clicks over the archive period + ('RainRate', 'H'), + # Highest rain rate over the archive period, or the rate + # shown on the console at the end of the period if there + # was no rain. Units are (rain clicks / hour) + ('RainRateHi', 'H'), + # Barometer reading at the end of the archive period. + # Units are (in Hg / 1000). + ('Barometer', 'H'), + # Average Solar Rad over the archive period. + # Units are (Watts / m 2 ) + ('SolarRad', 'H'), + # Number of packets containing wind speed data + # received from the ISS or wireless anemometer. + ('WindSamps', 'H'), + # Either the Average Inside Temperature, or the Final + # Inside Temperature over the archive period. Units + # are (F / 10) + ('TempIn', 'H'), + # Inside Humidity at the end of the archive period + ('HumIn', 'B'), + # Outside Humidity at the end of the archive period + ('HumOut', 'B'), + # Average Wind Speed over the archive interval. Units are (MPH) + ('WindAvg', 'B'), + # Highest Wind Speed over the archive interval. Units are (MPH) + ('WindHi', 'B'), + # Direction code of the High Wind speed. 0 = N, 1 = NNE, 2 = NE, ... 14 = NW, 15 = NNW, 255 = Dashed + ('WindHiDir', 'B'), + # Prevailing or Dominant Wind Direction code. + # 0 = N, 1 = NNE, 2 = NE, ... 14 = NW, 15 = NNW, 255 = Dashed + # Firmware before July 8th 2001 does not report direction code 255 + ('WindAvgDir', 'B'), + # Average UV Index. Units are (UV Index / 10) + ('UV', 'B'), + # ET accumulated over the last hour. Only records "on the hour" will have a non-zero value. Units are (in /1000) + ('ETHour', 'B'), + # Highest Solar Rad's value over the archive period. Units are (Watts / m 2) + ('SolarRadHi', 'H'), + # Highest UV Index value over the archive period. + ('UVHi', 'B'), + # Weather forecast rule at the end of the archive period. + ('ForecastRuleNo', 'B'), + # 2 Leaf Temperature values. Units are (F + 90) + ('LeafTemps', '2s'), + # 2 Leaf Wetness values. Range is 0-15 + ('LeafWetness', '2s'), + # 4 Soil Temperatures. Units are (F + 90) + ('SoilTemps', '4s'), + # 0xFF = Rev A, 0x00 = Rev B archive record + ('RecType', 'B'), + # 2 Extra Humidity values + ('ExtraHum', '2s'), + # 3 Extra Temperature values. Units are (F + 90) + ('ExtraTemps', '3s'), + # 4 Soil Moisture values. Units are (cb) + ('SoilMoist', '4s'), + ) + + def _post_unpack(self, items): + items = super(_ArchiveBStruct, self)._post_unpack(items) + items['LeafTemps'] = tuple( + t - 90 for t in struct.unpack('2B', items['LeafTemps'])) + items['LeafWetness'] = struct.unpack('2B', items['LeafWetness']) + items['ExtraTemps'] = tuple( + t - 90 for t in struct.unpack('3B', items['ExtraTemps'])) + return items + + +# --------------------------------------------------------------------------- # + +# simple data structures +DmpStruct = Struct( + (('Pages', 'H'), ('Offset', 'H'), ('CRC', 'H')), + order='=') + +DmpPageStruct = Struct( + (('Index', 'B'), ('Records', '260s'), ('unused', '4B'), ('CRC', 'H')), + order='=') + + +class _TimeStruct(Struct): + FMT = ( + ('Sec', 'B'), + ('Min', 'B'), + ('Hour', 'B'), + ('Day', 'B'), + ('Month', 'B'), + ('Year', 'B'), + ('CRC', 'H'), + ) + + def __init__(self): + super(_TimeStruct, self).__init__(self.FMT, '=') + + def _post_unpack(self, items): + items['Year'] = items['Year'] + 1900 + return items + + +# init structure classes +LoopStruct = LoopStruct() +ArchiveAStruct = _ArchiveAStruct() +ArchiveBStruct = _ArchiveBStruct() +timeStruct = _TimeStruct() + + +############################################################################## +# |--------------------------------------------------------------------------|# +# |--------------------------------------------------------------------------|# +# | API for the Davis Vantage Pro |# +# |--------------------------------------------------------------------------|# +# |--------------------------------------------------------------------------|# +############################################################################## + +class VantagePro(Station): + """ + A class capable of reading raw (binary) weather data from a + vantage pro console and parsing it into usable scalar + (integer/long/real) values. + + The data read from the console is in binary format. The data is in + least-ordered nybble strategy, and must be read with correct sizes and + offsets for proper byte ordering. + """ + + # device reply commands + WAKE_ACK = '\n\r' + ACK = '\x06' + ESC = '\x1b' + OK = '\n\rOK\n\r' + + # archive format type, unknown + _ARCHIVE_REV_B = None + + def __init__( + self, + device, + log_interval=5, + log_start_date=None, + clear=False + ): + """ + Initialize the serial connection with the console. + :param device: /dev/yourConsoleDevice + :param log_interval: default 5 + :param log_start_date: the datetime.datetime object representing the + starting log date. Default None aka "all" + :param clear: boolean, if true clean all the log in the console. + Default False. + """ + self.port = serial.Serial(device, BAUD, timeout=READ_DELAY) + # set the logging interval to be downloaded. Default all + if log_start_date is None: + self._archive_time = (0, 0) + else: + self._archive_time = (self.calcDateStamp(log_start_date), + self.calcTimeStamp(log_start_date)) + + if clear: + self._cmd('CLRLOG') # prevent getting a full log dump at startup + self._cmd('SETPER', log_interval, ok=True) + + self.fields = {} + + @staticmethod + def calcDateStamp(date): + """ + As stated into the Vantage Serial Protocol manual, this method converts + a datetime object into the right DateStamp + + :param date: the datetime object to convert + :return: the dateStamp integer + """ + return date.day + date.month * 32 + (date.year - 2000) * 512 + + @staticmethod + def calcTimeStamp(date): + """ + As stated into the Vantage Serial Protocol manual, this method converts + a datetime object into the right TimeStamp. + + :param date: the datetime object to convert + :return: the timeStamp integer + """ + return 100 * date.hour + date.minute + + def __del__(self): + """ + close serial port when object is deleted. + """ + self.port.close() + + def _use_rev_b_archive(self, records, offset): + """ + return True if weather station returns Rev.B archives + """ + # if pre-determined, return result + if type(self._ARCHIVE_REV_B) is bool: + return self._ARCHIVE_REV_B + # assume, B and check 'RecType' field + data = ArchiveBStruct.unpack_from(records, offset) + if data['RecType'] == 0: + log.info('detected archive rev. B') + self._ARCHIVE_REV_B = True + else: + log.info('detected archive rev. A') + self._ARCHIVE_REV_B = False + + return self._ARCHIVE_REV_B + + def _wakeup(self) -> None: + """ + issue wakeup command to device to take out of standby mode. + """ + log.info("send: WAKEUP") + + awake, i = False, 0 + while not awake and i < 3: + self.port.write("\n".encode()) + ack = self.port.read(len(self.WAKE_ACK)) + if ack.decode() == self.WAKE_ACK: + awake = True + else: + time.sleep(1.2) + i += 1 + + try: + assert awake is True + except AssertionError: + raise NoDeviceException('Can not access weather station') + + return None + + def _cmd(self, cmd, *args, **kw) -> None: + """ + write a single command, with variable number of arguments. after the + command, the device must return ACK + """ + ok = kw.setdefault('ok', False) + + self._wakeup() + if args: + cmd = "%s %s" % (cmd, ' '.join(str(a) for a in args)) + for i in range(3): + log.info("send: " + cmd) + self.port.write(f"{cmd} \n".encode()) + if ok: + ack = self.port.read(len(self.OK)) # read OK + # log_raw('read', ack) + if ack == self.OK: + return + else: + ack = self.port.read(len(self.ACK)) # read ACK + # log_raw('read', ack) + if ack.decode() == self.ACK: + return + # raise NoDeviceException('Can not access weather station') + + def _loop_cmd(self): + """ + Reads a raw string containing data read from the device + provided (in /dev/XXX) format. All reads are non-blocking. + """ + self._cmd('LOOP', 1) + raw = self.port.read(LoopStruct.size) # read data + return raw + + def _dmpaft_cmd(self, time_fields): + """ + issue a command to read the archive records after a known time stamp. + """ + records = [] + # convert time stamp fields to buffer + tbuf = struct.pack('2H', *time_fields) + + # 1. send 'DMPAFT' cmd + self._cmd('DMPAFT') + + # 2. send time stamp + crc + crc = VProCRC.get(tbuf) + crc = struct.pack('>H', crc) # crc in big-endian format + self.port.write(tbuf + crc) # send time stamp + crc + ack = self.port.read(len(self.ACK)) # read ACK + if ack.decode() != self.ACK: + return None # if bad ack, return None + + # 3. read pre-amble data + raw = self.port.read(DmpStruct.size) + if not VProCRC.verify(raw): # check CRC value + self.port.write(self.ESC) # if bad, escape and abort + return + self.port.write(self.ACK.encode()) # send ACK + + # 4. loop through all page records + dmp = DmpStruct.unpack(raw) + log.info('reading %d pages, start offset %d' % + (dmp['Pages'], dmp['Offset'])) + for i in range(dmp['Pages']): + # 5. read page data + raw = self.port.read(DmpPageStruct.size) + if not VProCRC.verify(raw): # check CRC value + self.port.write(self.ESC) # if bad, escape and abort + return + self.port.write(self.ACK.encode()) # send ACK + + # 6. loop through archive records + page = DmpPageStruct.unpack(raw) + offset = 0 # assume offset at 0 + if i == 0: + offset = dmp['Offset'] * ArchiveAStruct.size + while offset < ArchiveAStruct.size * 5: + log.info('page %d, reading record at offset %d' % + (page['Index'], offset)) + if self._use_rev_b_archive(page['Records'], offset): + a = ArchiveBStruct.unpack_from(page['Records'], offset) + else: + a = ArchiveAStruct.unpack_from(page['Records'], offset) + # 7. verify that record has valid data, and store + if a['DateStamp'] != 0xffff and a['TimeStamp'] != 0xffff: + records.append(a) + offset += ArchiveAStruct.size + log.info('read all pages') + return records + + def _get_loop_fields(self): + crc_ok = None + for i in range(3): + raw = self._loop_cmd() # read raw data + crc_ok = VProCRC.verify(raw) + if crc_ok: + break # exit loop if valid + time.sleep(1) + + if not crc_ok: + raise NoDeviceException('Can not access weather station') + + return LoopStruct.unpack(raw) + + def _get_new_archive_fields(self): + """ + returns a dictionary of fields from the newest archive record in the + device. return None when no records are new. + """ + records = [] + for i in range(3): + records = self._dmpaft_cmd(self._archive_time) + if records is not None: + break + time.sleep(1) + + if records is None: + raise NoNewRecordsException('Can not download any new record.') + + # find the newest record + new_rec = None + for r in records: + new_time = (r['DateStamp'], r['TimeStamp']) + if self._archive_time < new_time: + self._archive_time = new_time + new_rec = r + + return new_rec + + @staticmethod + def _calc_derived_fields(fields): + """ + calculates the derived fields (those fields that are calculated) + """ + # convenience variables for the calculations below + temp_ = fields['TempOut'] + hum = fields['HumOut'] + wind_ = fields['WindSpeed'] + wind10min = fields['WindSpeed10Min'] + fields['HeatIndex'] = calc_heat_index(temp_, hum) + fields['WindChill'] = calc_wind_chill(temp_, wind_, wind10min) + fields['DewPoint'] = calc_dewpoint(temp_, hum) + # store current data string + now = time.localtime() + fields['DateStamp'] = time.strftime("%Y-%m-%d %H:%M:%S", now) + fields['Year'] = now[0] + fields['Month'] = str(now[1]).zfill(2) + now = time.gmtime() + fields['DateStampUtc'] = time.strftime("%Y-%m-%d %H:%M:%S", now) + fields['YearUtc'] = now[0] + fields['MonthUtc'] = str(now[1]).zfill(2) + + def parse(self): + """ + read and parse a set of data read from the console. after the + data is parsed it is available in the fields variable. + """ + fields = self._get_loop_fields() + # TODO: this will overwrite the last archived record with the newest record. + # Is this the expected behavior? + fields['Archive'] = self._get_new_archive_fields() + + self._calc_derived_fields(fields) + + # set the fields variable the values in the dict + self.fields = fields + + def get_reading(self) -> WeatherPoint: + """Return a single weather reading.""" + self.parse() + + return self._fields_to_weather_point(self.fields) + + @staticmethod + def _fields_to_weather_point(fields: dict) -> WeatherPoint: + """Convert VantagePro fields dictionary to WeatherPoint. + + Only supports limited subset of data available in self.fields - + generally only those useful for posting to weather services. + """ + return WeatherPoint( + temperature_f=fields['TempOut'], + pressure=fields['Pressure'], + dew_point_f=fields['DewPoint'], + humidity=fields['HumOut'], + + rain_rate_in=fields['RainRate'], + rain_day_in=fields['RainDay'], + time=dt.datetime.strptime(fields['DateStampUtc'], "%Y-%m-%d %H:%M:%S"), + wind_speed_mph=fields['WindSpeed10Min'], + wind_direction=fields['WindDir'], + ) diff --git a/davisAPI/PyWeather-master/weather/stations/netatmo.py b/davisAPI/PyWeather-master/weather/stations/netatmo.py new file mode 100644 index 0000000..e0d74c2 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/stations/netatmo.py @@ -0,0 +1,74 @@ +'''Netatmo Weather Station support. + +https://www.netatmo.com/en-eu/weather/weatherstation + +Prep: + +1. [Create a Netatmo app](https://dev.netatmo.com/apps/createanapp#form). +2. Use generated app id and secret as `client_id` and `client_secret`. +3. Use your own Netatmo username (e-mail) as `username` and `password`. + +Example usage: + +station = NetatmoStation(cliend_id='deadbeef1234567890abcdef', + client_secret='123', + username='me@example.com', + password='correcthorse') + +pprint.pprint(station.get_reading()) +''' + +import datetime + +from weather.stations.station import * + +import pyatmo + + +class NetatmoStation(Station): + '''Netatmo Weather Station support. + + Implementes as a relatively thin wrapper on top of pyatmo. + ''' + + def __init__(self, client_id: str, client_secret: str, username: str, + password: str, module_name: str = 'Outdoor', **kwargs): + '''Initialize and auth Netatmo Weatehr station reader. + + See module docstring for agrs explanation. + ''' + if kwargs: + raise ValueError("Unknown params: %s" % ",".join(kwargs.keys())) + self.module_name = module_name + self._auth = pyatmo.ClientAuth( + client_id=client_id, + client_secret=client_secret, + username=username, + password=password) + + def get_reading(self) -> WeatherPoint: + '''Return single weather reading. + + Currently only assumes account only has one Netatmo station with + one module. Does not support anemometer or rain gauge. + ''' + weatherData = pyatmo.WeatherStationData(self._auth) + # We assume there is only one station in the account. + station_id = next(iter(weatherData.stations.values()))['_id'] + module_id = None + for module in weatherData.get_modules(station_id).values(): + if module['module_name'] == self.module_name: + module_id = module['id'] + break + if module_id is None: + raise ValueError( + 'Module %s not found, found %s' % ( + self.module_name, + weatherData.get_module_names(station_id))) + + module_data = weatherData.get_module(module_id) + + return WeatherPoint( + temperature_c=module_data['dashboard_data']['Temperature'], + humidity=module_data['dashboard_data']['Humidity'], + time=datetime.datetime.fromtimestamp(module_data['last_message'])) diff --git a/davisAPI/PyWeather-master/weather/stations/station.py b/davisAPI/PyWeather-master/weather/stations/station.py new file mode 100644 index 0000000..61bde38 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/stations/station.py @@ -0,0 +1,104 @@ +"""Base class for all Weather Station implementations.""" + +import datetime +import time as time_module + +from ..units.temp import fahrenheit_to_celsius, celsius_to_fahrenheit + +__all__ = ['WeatherPoint', 'Station'] + + +class WeatherPoint: + """ + Represents a single weather measurement. + """ + time: datetime.datetime = None + + _temperature_c: float = None # Temperature in Celsius + _temperature_f: float = None # Temperature in Fahrenheit + humidity: int = None # Relative humidity in percent + dew_point_f: float = None # Dew point in Fahrenheit + pressure: float = None # Atmospheric pressure + + rain_rate_in: float = None # Rain rate in inches + rain_day_in: float = None # Rain inches so far today + + wind_speed_mph: float = None # Wind's speed in miles per hour + wind_direction: int = None # Wind's direction, in degrees + + def __init__( + self, + time=None, + temperature_c: float = None, + temperature_f: float = None, + humidity: int = None, + dew_point_f: float = None, + pressure: float = None, + rain_rate_in: float = None, + rain_day_in: float = None, + wind_speed_mph: float = None, + wind_direction: int = None, + ): + self.time = time or time_module.gmtime() + if temperature_c is not None and temperature_f is not None: + raise ValueError('Only one of temperature_c and temperature_f can be passed.') + if temperature_c is not None: + self.temperature_c = temperature_c + else: + self.temperature_f = temperature_f + self.humidity = humidity + self.dew_point_f = dew_point_f + self.pressure = pressure + self.rain_rate_in = rain_rate_in + self.rain_day_in = rain_day_in + self.wind_speed_mph = wind_speed_mph + self.wind_direction = wind_direction + + @property + def temperature_f(self) -> float: + if self._temperature_f is not None: + return self._temperature_f + elif self._temperature_c is not None: + return celsius_to_fahrenheit(self._temperature_c) + + @temperature_f.setter + def temperature_f(self, value: float): + self._temperature_f = value + self._temperature_c = None + + @property + def temperature_c(self) -> float: + if self._temperature_c is not None: + return self._temperature_c + elif self._temperature_f is not None: + return fahrenheit_to_celsius(self._temperature_f) + + @temperature_c.setter + def temperature_c(self, value: float): + self._temperature_f = None + self._temperature_c = value + + def __eq__(self, other): + return ( + self.time == other.time and + self._temperature_c == other._temperature_c and + self._temperature_f == other._temperature_f and + self.humidity == other.humidity and + self.dew_point_f == other.dew_point_f and + self.pressure == other.pressure and + self.rain_rate_in == other.rain_rate_in and + self.rain_day_in == other.rain_day_in and + self.wind_speed_mph == other.wind_speed_mph and + self.wind_direction == other.wind_direction + ) + + def __repr__(self): + return str(self.__dict__) + + +class Station: + """Base class for all Weather Station implementations.""" + + def get_reading(self) -> WeatherPoint: + """Returns a single weather point.""" + raise NotImplementedError('Not implemented') diff --git a/davisAPI/PyWeather-master/weather/stations/tests/__init__.py b/davisAPI/PyWeather-master/weather/stations/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/davisAPI/PyWeather-master/weather/stations/tests/test_davis.py b/davisAPI/PyWeather-master/weather/stations/tests/test_davis.py new file mode 100644 index 0000000..416d21f --- /dev/null +++ b/davisAPI/PyWeather-master/weather/stations/tests/test_davis.py @@ -0,0 +1,116 @@ + + +import codecs +import datetime +import mock +import unittest + +from ..davis import VProCRC, VantagePro, LoopStruct, _fields_to_weather_point +from ..station import WeatherPoint + +loop_data = ( + b"4c4f4f14003e032175da0239d10204056301ffffffffffffffffffff" + b"ffffffffff4effffffffffffff0000ffff7f0000ffff000000000000000000000000ffff" + b"ffffffffff0000000000000000000000000000000000002703064b26023e070a0d1163") + + +class TestCRC(unittest.TestCase): + + def test_crc(self): + raw = codecs.decode(loop_data, 'hex') + result = VProCRC.verify(raw) + self.assertTrue(result) + + +class TestParse(unittest.TestCase): + cmd_mock = mock.Mock() # for mocking '_cmd' method in 'vp' + loop_mock = mock.Mock() # for mocking '_loop_cmd' method in 'vp' + + def test_unpack_loop_data(self): + LoopStruct.unpack(codecs.decode(loop_data, 'hex')) + + @mock.patch.object(VantagePro, '_cmd', cmd_mock) + @mock.patch.object(VantagePro, '_loop_cmd', loop_mock) + def test_fields(self): + self.loop_mock.return_value = codecs.decode(loop_data, 'hex') + # TODO: this is working just if there is an actual weather station attached. + vp = VantagePro('/dev/ttyUSB0') + fields = vp._get_loop_fields() + + self.assertAlmostEqual(fields['Pressure'], 29.98499999) + self.assertAlmostEqual(fields['TempIn'], 73.0) + self.assertEqual(fields['HumIn'], 57) + self.assertAlmostEqual(fields['TempOut'], 72.09999999999) + self.assertEqual(fields['WindSpeed'], 4) + self.assertEqual(fields['WindSpeed10Min'], 5) + self.assertEqual(fields['WindDir'], 355) + self.assertEqual(fields['HumOut'], 78) + self.assertEqual(fields['RainRate'], 0.0) + self.assertEqual(fields['UV'], 0xFF) + self.assertEqual(fields['SolarRad'], 0x7FFF) + self.assertEqual(fields['RainStorm'], 0) + self.assertEqual(fields['StormStartDate'], '2127-15-31') + self.assertEqual(fields['RainDay'], 0) + self.assertEqual(fields['RainMonth'], 0) + self.assertEqual(fields['RainYear'], 0) + self.assertEqual(fields['ETDay'], 0) + self.assertEqual(fields['ETMonth'], 0) + self.assertEqual(fields['ETYear'], 0) + self.assertEqual(fields['SoilMoist'], (0xFF, 0xFF, 0xFF, 0xFF)) + self.assertEqual(fields['LeafWetness'], (0xFF, 0xFF, 0xFF, 0)) + self.assertEqual(fields['BatteryStatus'], 0) + self.assertAlmostEqual(fields['BatteryVolts'], 4.728515625) + self.assertEqual(fields['ForecastIcon'], 6) + self.assertEqual(fields['ForecastRuleNo'], 75) + self.assertEqual(fields['SunRise'], '05:50') + self.assertEqual(fields['SunSet'], '18:54') + + @mock.patch.object(VantagePro, '_cmd', cmd_mock) + @mock.patch.object(VantagePro, '_loop_cmd', loop_mock) + def test_derived_fields(self): + self.loop_mock.return_value = codecs.decode(loop_data, 'hex') + # TODO same as todo above! + vp = VantagePro('/dev/ttyUSB0') + fields = vp._get_loop_fields() + vp._calc_derived_fields(fields) + + self.assertAlmostEqual(fields['HeatIndex'], 72.09999999) + self.assertAlmostEqual(fields['WindChill'], 74.17574285) + self.assertAlmostEqual(fields['DewPoint'], 64.97343800) + self.assertNotEqual(fields['DateStamp'], '') + self.assertTrue(fields['Year'] > 2000) + self.assertTrue(1 <= int(fields['Month']) <= 12) + self.assertNotEqual(fields['DateStampUtc'], '') + self.assertTrue(fields['YearUtc'] > 2000) + self.assertTrue(1 <= int(fields['MonthUtc']) <= 12) + + +class TestFieldsToWeatherPoint(unittest.TestCase): + + def test_fields_to_weather_point(self): + fields = { + 'TempOut': 87, + 'Pressure': 29.9, + 'DewPoint': 80, + 'HumOut': 56, + 'RainRate': 0.1, + 'RainDay': 0.2, + 'DateStampUtc': '2020-01-02 03:04:05', + 'WindSpeed10Min': 5, + 'WindDir': 15 + } + expected = WeatherPoint( + time=datetime.datetime(2020, 1, 2, 3, 4, 5), + temperature_f=87, + humidity=56, + dew_point_f=80, + pressure=29.9, + rain_rate_in=0.1, + rain_day_in=0.2, + wind_speed_mph=5, + wind_direction=15 + ) + self.assertEqual(_fields_to_weather_point(fields), expected) + + +# vim: sts=4:ts=4:sw=4 diff --git a/davisAPI/PyWeather-master/weather/stations/tests/test_station.py b/davisAPI/PyWeather-master/weather/stations/tests/test_station.py new file mode 100644 index 0000000..4be95b5 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/stations/tests/test_station.py @@ -0,0 +1,17 @@ +'''Tests for the station module.''' + +import unittest + +from ..station import WeatherPoint + + +class WeatherPointTest(unittest.TestCase): + + def test_temperature_conversion(self): + w_f = WeatherPoint(temperature_f=80) + self.assertEqual(w_f.temperature_f, 80) + self.assertAlmostEqual(w_f.temperature_c, 26.666688) + + w_c = WeatherPoint(temperature_c=21) + self.assertEqual(w_c.temperature_c, 21) + self.assertAlmostEqual(w_c.temperature_f, 69.8) diff --git a/davisAPI/PyWeather-master/weather/stations/validate.py b/davisAPI/PyWeather-master/weather/stations/validate.py new file mode 100644 index 0000000..b4abb99 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/stations/validate.py @@ -0,0 +1,28 @@ + +class Validator: + def __init__(self, fields): + self.fields = fields + + def get_value(self, field, default): + return self.fields.get(field, default) + + def validate(self): + assert 0 <= self.get_value('HumOut', -1) <= 100 + assert 0 <= self.get_value('HumIn', -1) <= 100 + assert -120 <= self.get_value('DewPoint', -1) <= 254 + + assert -20 <= self.get_value('TempIn', -255) <= 254 + assert -120 <= self.get_value('TempOut', -255) <= 254 + assert -254 <= self.get_value('WindChill', -255) <= 254 + assert -120 <= self.get_value('HeatIndex', -255) <= 254 + + assert 0 <= self.get_value('RainYear', -1) <= 254 + assert 0 <= self.get_value('RainMonth', -1) <= 254 + assert 0 <= self.get_value('RainDay', -1) <= 254 + assert 0 <= self.get_value('RainStorm', -1) <= 254 + + assert 0 <= self.get_value('WindSpeed', -1) <= 200 + assert 0 <= self.get_value('WindSpeed10Min', -1) <= 200 + assert 0 <= self.get_value('WindDir', -1) <= 359 + + assert 26.00 <= self.get_value('Pressure', -1) <= 34.00 diff --git a/davisAPI/PyWeather-master/weather/units/__init__.py b/davisAPI/PyWeather-master/weather/units/__init__.py new file mode 100644 index 0000000..44f5e38 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/units/__init__.py @@ -0,0 +1,4 @@ +from .temp import * +from .precip import * +from .wind import * +from .pressure import * diff --git a/davisAPI/PyWeather-master/weather/units/astro.py b/davisAPI/PyWeather-master/weather/units/astro.py new file mode 100644 index 0000000..6664f7a --- /dev/null +++ b/davisAPI/PyWeather-master/weather/units/astro.py @@ -0,0 +1,103 @@ +#! /usr/bin/env python + +from math import * + +# 90 degrees 50 minutes +zenith = -0.01454 + + +def radians_to_degrees(radians): + return radians * (180 / pi) + +def degrees_to_radians(degrees): + return (degrees * pi) / 180 + + +def _daylight(n, t, lat, long, tz, day, month, year, rise=1): + lngHour = int / 15.0 + + # calculate the Sun's mean anomaly + m = (0.9856 * t) - 3.289 + + # calculate the Sun's true longitude + opA = 1.916 * sin(radians(m)) + opB = 0.020 * sin(2 * radians(m)) + l = m + opA + opB + 282.634 + if l > 360: + l = l - 360 + if l < 0: + l = l + 360 + + # calculate the Sun's right ascension + foo = 0.91764 * tan(degrees_to_radians(l)) + ra = radians_to_degrees(atan(foo)) + + # right ascension value needs to be in the same quadrant as l + lQuandrant = (floor(l / 90)) * 90 + raQuandrant = (floor(ra / 90)) * 90 + ra = ra + (lQuandrant - raQuandrant) + + # right ascension value needs to converted to hours + ra = ra / 15 + + # calculate the Sun's declination + sinDec = 0.39782 * sin(radians(l)) + cosDec = cos(asin(sinDec)) + + # calculate the Sun's local hour angle + numerator = sinDec * sin(radians(lat)) + denomenator = cosDec * cos(radians(lat)) + cosH = (zenith - numerator) / denomenator + + if (cosH > 1) or (cosH < -1): + # the sun don't shine here son (on the specified date) + return None + + # finish calulating h and convert into hours + if rise: + h = 360 - radians_to_degrees(acos(cosH)) + else: + h = radians_to_degrees(acos(cosH)) + h = h / 15 + + # calculate local mean time of rising / setting + T = h + ra - (0.06571 * t) - 6.622 + + # adjust back to UTC + UT = T - lngHour + + # convert UT value to local time zone of lat/long + localT = UT + tz + + hour = int(localT) + min = 60 * (localT % hour) + if hour < 0: + hour = 24 + hour - 1 + if min < 0: + min = 60 + min + return (year, month, day, hour, min, 0, 0, 0, 0) + + +def daylight(lat, long, tz, day, month, year): + # calc the day of the year + n1 = floor(275 * month / 9) + n2 = floor((month + 9) / 12) + n3 = (1 + floor((year - 4 * floor(year / 4) + 2) / 3)) + n = n1 - (n2 * n3) + day - 30 + + # convert the long to hour value and calc an approximate time + lngHour = int / 15.0 + + t = n + ((6 - lngHour) / 24.0) + sunrise = _daylight(n, t, lat, int, tz, day, month, year, 1) + + t = n + ((18 - lngHour) / 24.0) + sunset = _daylight(n, t, lat, int, tz, day, month, year, 0) + + return (sunrise, sunset) + + +if __name__ == '__main__': + sunrise, sunset = daylight(40.9, -74.3, -4, 14, 4, 2003) + print('sunrise is ' + str(sunrise)) + print('sunset is ' + str(sunset)) diff --git a/davisAPI/PyWeather-master/weather/units/precip.py b/davisAPI/PyWeather-master/weather/units/precip.py new file mode 100644 index 0000000..1225b5b --- /dev/null +++ b/davisAPI/PyWeather-master/weather/units/precip.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +# +# Precipitation conversions +# +# -Christopher Blunck +# + +import sys + +__author__ = 'Christopher Blunck' +__email__ = 'chris@wxnet.org' +__revision__ = '$Revision: 1.6 $' + +__doc__ = ''' +precipitation related conversions +''' + +__usage__ = '' + + diff --git a/davisAPI/PyWeather-master/weather/units/pressure.py b/davisAPI/PyWeather-master/weather/units/pressure.py new file mode 100644 index 0000000..3b5a333 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/units/pressure.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python + +# +# See __doc__ for an explanation of what this module does +# +# See __usage__ for an explanation of runtime arguments. +# +# -Christopher Blunck +# + + +__author__ = 'Christopher Blunck' +__email__ = 'chris@wxnet.org' +__revision__ = '$Revision: 1.6 $' + +__doc__ = 'pressure related conversion functions' +__usage__ = 'this module should not be run via the command line' + + +def atm_to_in32(atm): + """Atmospheres (atm) to inches of mercury @32F (inHg32)""" + return atm * 29.9213 + + +def atm_to_in60(atm): + """Atmospheres (atm) to inches of mercury @60F (inHg60)""" + return atm * 30.0058 + + +def atm_to_mb(atm): + """Atmospheres (atm) to millibars (mb)""" + return atm * 1013.25 + + +def atm_to_pa(atm): + """Atmospheres (atm) to pascals (Pa)""" + return atm * 101325 + + +def atm_to_lb_sqin(atm): + """Atmospheres (atm) to pounds/square inch (lb/in**2)""" + return atm * 14.696 + + +def in32_to_mb(inches): + """Inches of mercury @32F (inHg32) to millibars (mb)""" + return inches * 33.8639 + + +def in32_to_atm(inches): + """Inches of mercury @32F (inHg32) to millibars (mb)""" + return inches * 0.0334211 + + +def in32_to_lbs(inches): + """Inches of mercury @32F (inHg32) to pounds/square inch (lb/in**2)""" + return inches * 0.49115 + + +def in60_to_mb(inches): + """Inches of mercury @60F (inHg60) to atmospheres (atm)""" + return inches * 33.7685 + + +def in60_to_atm(inches): + """Inches of mercury @60F (inHg60) to millibars (mb)""" + return inches * 0.0333269 + + +def in60_to_lbs(inches): + """Inches of mercury @60F (inHg60) to pounds/square inch (lb/in**2)""" + return inches * 0.48977 + + +def incConv_to_Pa(inches): + """ + Inches of mercury to Pascals using the NIST conventional coefficient + :param inches: inches of mg + :return: pascals + """ + return inches * 3.386389 + + +def incConv_to_kPa(inches): + """ + Inches of mercury to kilo Pascals using the NIST conventional coefficient + :param inches: inches of mg + :return: pascals + """ + return incConv_to_Pa(inches) * 1000 + + +def mb_to_atm(mb): + """Millibars (mb) to atmospheres (atm)""" + return mb * 0.000986923 + + +def mb_to_hpa(mb): + """Millibars (mb) to hectopascals (hPa)""" + return mb + + +def mb_to_in32(mb): + """Millibars (mb) to inches of mercury @32F (inHg60)""" + return mb * 0.02953 + + +def mb_to_in60(mb): + """Millibars (mb) to inches of mercury @60F (inHg60)""" + return mb * 0.02961 + + +def mb_to_kpa(mb): + """Millibars (mb) to kilopascals (kPa)""" + return mb * 0.1 + + +def mb_to_mm32(mb): + """Millibars (mb) to millimeters of mercury @32F (mmHg)""" + return mb * 0.75006 + + +def mb_to_mm60(mb): + """Millibars (mb) to millimeters of mercury @60F (mmHg)""" + return mb * 0.75218 + + +def mb_to_n_sqm(mb): + """Millibars (mb) to newtons/square meter (N/m**2)""" + return mb * 100 + + +def mb_to_pa(mb): + """Millibars (mb) to pascals (Pa)""" + return mb * 100 + + +def mb_to_lb_sqft(mb): + """Millibars (mb) to pounds/square foot (lb/ft**2)""" + return mb * 2.088543 + + +def mb_to_lb_sqin(mb): + """Millibars (mb) to pounds/square inch (lb/in**2)""" + return mb * 0.0145038 + + +def mm32_to_mb(mm32): + """Millimeters of mercury @32F (mmHg) to millibars (mb)""" + return mm32 * 1.33322 + + +def mm60_to_mb(mm60): + """Millimeters of mercury @60F (mmHg) to millibars (mb)""" + return mm60 * 1.32947 + + +def n_sqm_to_mb(nsqm): + """Newtons/square meter (N/m**2) to millibars (mb)""" + return nsqm * 0.01 + + +def pa_to_atm(pa): + """Pascals (Pa) to atmospheres (atm)""" + return pa * 0.000009869 + + +def pa_to_mb(pa): + """Pascals (Pa) to millibars (mb)""" + return pa * 0.01 + + +def hpa_to_mb(hpa): + """Hectopascals (hPa) to millibars (mb)""" + return hpa + + +def kpa_to_mb(hpa): + """Kilopascals (kPa) to millibars (mb)""" + return hpa * 10 + + +def lb_sqft_to_mb(lbs): + """Pounds/square foot (lb/ft**2) to millibars (mb)""" + return lbs * 0.478803 + + +def lb_sqin_to_atm(lbs): + """Pounds/square inch (lb/in**2) to atmospheres (atm)""" + return lbs * 0.068046 + + +def lb_sqin_to_mm32(lbs): + """Pounds/square inch (lb/in**2) to inches of mercury @32F (inHg32)""" + return lbs * 2.03602 + + +def lb_sqin_to_mm60(lbs): + """Pounds/square inch (lb/in**2) to inches of mercury @60F (inHg60)""" + return lbs * 2.04177 + + +def lb_sqin_to_mb(lbs): + """Pounds/square inch (lb/in**2) to millibars (mb)""" + return lbs * 68.9474483 + + +def hpa_to_inches(hpa): + return hpa / 33.87 diff --git a/davisAPI/PyWeather-master/weather/units/temp.py b/davisAPI/PyWeather-master/weather/units/temp.py new file mode 100644 index 0000000..fed1a31 --- /dev/null +++ b/davisAPI/PyWeather-master/weather/units/temp.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python + +# +# See __doc__ for an explanation of what this module does +# +# See __usage__ for an explanation of runtime arguments. +# +# -Christopher Blunck +# + +import math +import sys + +__author__ = 'Christopher Blunck' +__email__ = 'chris@wxnet.org' +__revision__ = '$Revision: 1.6 $' + +__doc__ = 'temperature related conversion functions' +__usage__ = 'this module should not be run via the command line' + + +def celsius_to_fahrenheit(c): + """Degrees Celsius (C) to degrees Fahrenheit (F)""" + return (c * 1.8) + 32.0 + + +def celsius_to_kelvin(c): + """Degrees Celsius (C) to degrees Kelvin (K)""" + return c + 273.15 + + +def celsius_to_rankine(c): + """Degrees Celsius (C) to degrees Rankine (R)""" + return (c * 1.8) + 491.67 + + +def fahrenheit_to_celsius(f): + """Degrees Fahrenheit (F) to degrees Celsius (C)""" + return (f - 32.0) * 0.555556 + + +def fahrenheit_to_kelvin(f): + """Degrees Fahrenheit (F) to degrees Kelvin (K)""" + return (f * 0.555556) + 255.37 + + +def fahrenheit_to_rankine(f): + """Degrees Fahrenheit (F) to degrees Rankine (R)""" + return f + 459.67 + + +def kelvin_to_celsius(k): + """Degrees Kelvin (K) to degrees Celsius (C)""" + return k - 273.15 + + +def kelvin_to_fahrenheit(k): + """Degrees Kelvin (K) to degrees Fahrenheit (F)""" + return (k - 255.37) * 1.8 + + +def kelvin_to_rankine(k): + """Degrees Kelvin (K) to degrees Rankine (R)""" + return k * 1.8 + + +def rankine_to_celsius(r): + """Degrees Rankine (R) to degrees Celsius (C)""" + return (r - 491.67) * 0.555556 + + +def rankine_to_fahrenheit(r): + """Degrees Rankine (R) to degrees Fahrenheit (F)""" + return r - 459.67 + + +def rankine_to_kelvin(r): + """Degrees Rankine (R) to degrees Kelvin (K)""" + return r * 0.555556 + + +def calc_heat_index(temp, hum): + """ + calculates the heat index based upon temperature (in F) and humidity. + http://www.srh.noaa.gov/bmx/tables/heat_index.html + + returns the heat index in degrees F. + """ + + if temp < 80: + return temp + else: + return -42.379 + 2.04901523 * temp + 10.14333127 * hum - 0.22475541 * \ + temp * hum - 6.83783 * (10 ** -3) * (temp ** 2) - 5.481717 * \ + (10 ** -2) * (hum ** 2) + 1.22874 * (10 ** -3) * (temp ** 2) * \ + hum + 8.5282 * (10 ** -4) * temp * (hum ** 2) - 1.99 * \ + (10 ** -6) * (temp ** 2) * (hum ** 2) + + +def calc_wind_chill(t, windspeed, windspeed10min=None): + """ + calculates the wind chill value based upon the temperature (F) and + wind. + + returns the wind chill in degrees F. + """ + + w = max(windspeed10min or 0, windspeed) + return 35.74 + 0.6215 * t - 35.75 * (w ** 0.16) + 0.4275 * t * (w ** 0.16) + + +def calc_humidity(temp, dewpoint): + """ + calculates the humidity via the formula from weatherwise.org + return the relative humidity + """ + + t = fahrenheit_to_celsius(temp) + td = fahrenheit_to_celsius(dewpoint) + + num = 112 - (0.1 * t) + td + denom = 112 + (0.9 * t) + + rh = math.pow((num / denom), 8) + + return rh + + +def calc_dewpoint(temp, hum): + """ + calculates the dewpoint via the formula from weatherwise.org + return the dewpoint in degrees F. + """ + + c = fahrenheit_to_celsius(temp) + x = 1 - 0.01 * hum + + dewpoint = (14.55 + 0.114 * c) * x + dewpoint = dewpoint + ((2.5 + 0.007 * c) * x) ** 3 + dewpoint = dewpoint + (15.9 + 0.117 * c) * x ** 14 + dewpoint = c - dewpoint + + return celsius_to_fahrenheit(dewpoint) + + +def calc_dewpoint_davis(temp, hum): + ''' + calculate the dewpoint via the formula used by Davis. See Davis Application Note 28 - Derived Variables in Davis + Weather Products. + :author: Paolo Bellagente (p.bellagente@unibs.it - 23/03/2017 + :param temp: Outside temperature in F + :param hum: Relative outside humidity + :return: the dewpoint in F + ''' + v = hum * 0.01 * 6.112 * math.exp((17.62 * temp) / (temp + 243.12)) + n = 243.12 * (math.log(v)) - 440.1 + d = 19.43 - math.log(v) + return n / d diff --git a/davisAPI/PyWeather-master/weather/units/tests/__init__.py b/davisAPI/PyWeather-master/weather/units/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/davisAPI/PyWeather-master/weather/units/tests/test_pressure.py b/davisAPI/PyWeather-master/weather/units/tests/test_pressure.py new file mode 100644 index 0000000..53dbd8a --- /dev/null +++ b/davisAPI/PyWeather-master/weather/units/tests/test_pressure.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python + +# +# Unit Tests for pressure module +# +# See __usage__ for an explanation of runtime arguments. +# +# -Christopher Blunck +# + + +import unittest + +from ..pressure import * + +__author__ = 'Christopher Blunck' +__email__ = 'chris@wxnet.org' +__revision__ = '$Revision: 1.6 $' + +__doc__ = ''' +Unit tests the pressure module. +''' + +__usage__ = ''' + python $0 +''' + + +def usage(): + print(__usage__) + sys.exit(1) + + +class TestCase(unittest.TestCase): + def setUp(self): pass + + def tearDown(self): pass + + def test__atm_to_in32(self): + # make sure some hard-coded values work + assert round(atm_to_in32(1.0364), 4) == 31.0104, "value not correct" + assert round(atm_to_in32(1.0000), 4) == 29.9213, "value not correct" + assert round(atm_to_in32(0.9268), 4) == 27.7311, "value not correct" + assert round(atm_to_in32(0.8883), 4) == 26.5791, "value not correct" + + def test__atm_to_in60(self): + # make sure some hard-coded values work + assert round(atm_to_in60(1.0364), 4) == 31.0980, "value not correct" + assert round(atm_to_in60(1.0000), 4) == 30.0058, "value not correct" + assert round(atm_to_in60(0.9268), 4) == 27.8094, "value not correct" + assert round(atm_to_in60(0.8883), 4) == 26.6542, "value not correct" + + def test__atm_to_mb(self): + # make sure some hard-coded values work + assert round(atm_to_mb(1.0364), 4) == 1050.1323, "value not correct" + assert round(atm_to_mb(1.0000), 4) == 1013.2500, "value not correct" + assert round(atm_to_mb(0.9268), 4) == 939.0801, "value not correct" + assert round(atm_to_mb(0.8883), 4) == 900.0700, "value not correct" + + def test__atm_to_pa(self): + # make sure some hard-coded values work + assert round(atm_to_pa(1.0364), 4) == 105013.2300, "value not correct" + assert round(atm_to_pa(1.0000), 4) == 101325.0000, "value not correct" + assert round(atm_to_pa(0.9268), 4) == 93908.0100, "value not correct" + assert round(atm_to_pa(0.8883), 4) == 90006.9975, "value not correct" + + def test__atm_to_lb_sqin(self): + # make sure some hard-coded values work + assert round(atm_to_lb_sqin(1.0364), 4) == 15.2309, "value not correct" + assert round(atm_to_lb_sqin(1.0000), 4) == 14.6960, "value not correct" + assert round(atm_to_lb_sqin(0.9268), 4) == 13.6203, "value not correct" + assert round(atm_to_lb_sqin(0.8883), 4) == 13.0545, "value not correct" + + def test__in32_to_mb(self): + # make sure some hard-coded values work + assert int(in32_to_mb(31.01)) == 1050, "value not correct" + assert int(in32_to_mb(29.92)) == 1013, "value not correct" + assert int(in32_to_mb(27.73)) == 939, "value not correct" + assert int(in32_to_mb(26.58)) == 900, "value not correct" + + def test__in32_to_atm(self): + # make sure some hard-coded values work + assert round(in32_to_atm(31.01), 4) == 1.0364, "value not correct" + assert round(in32_to_atm(29.92), 4) == 1.0000, "value not correct" + assert round(in32_to_atm(27.73), 4) == 0.9268, "value not correct" + assert round(in32_to_atm(26.58), 4) == 0.8883, "value not correct" + + def test__in32_to_lbs(self): + # make sure some hard-coded values work + assert round(in32_to_lbs(31.01), 4) == 15.2306, "value not correct" + assert round(in32_to_lbs(29.92), 4) == 14.6952, "value not correct" + assert round(in32_to_lbs(27.73), 4) == 13.6196, "value not correct" + assert round(in32_to_lbs(26.58), 4) == 13.0548, "value not correct" + + def test__in60_to_mb(self): + # make sure some hard-coded values work + assert int(in60_to_mb(31.01)) == 1047, "value not correct" + assert int(in60_to_mb(29.92)) == 1010, "value not correct" + assert int(in60_to_mb(27.73)) == 936, "value not correct" + assert int(in60_to_mb(26.58)) == 897, "value not correct" + + def test__in60_to_atm(self): + # make sure some hard-coded values work + assert round(in60_to_atm(31.01), 4) == 1.0335, "value not correct" + assert round(in60_to_atm(29.92), 4) == 0.9971, "value not correct" + assert round(in60_to_atm(27.73), 4) == 0.9242, "value not correct" + assert round(in60_to_atm(26.58), 4) == 0.8858, "value not correct" + + def test__in60_to_lbs(self): + # make sure some hard-coded values work + assert round(in60_to_lbs(31.01), 4) == 15.1878, "value not correct" + assert round(in60_to_lbs(29.92), 4) == 14.6539, "value not correct" + assert round(in60_to_lbs(27.73), 4) == 13.5813, "value not correct" + assert round(in60_to_lbs(26.58), 4) == 13.0181, "value not correct" + + def test__mb_to_atm(self): + # make sure some hard-coded values work + assert round(mb_to_atm(1050), 4) == 1.0363, "value not correct" + assert round(mb_to_atm(1013), 4) == 0.9998, "value not correct" + assert round(mb_to_atm(939), 4) == 0.9267, "value not correct" + assert round(mb_to_atm(900), 4) == 0.8882, "value not correct" + + def test__mb_to_hpa(self): + # make sure some hard-coded values work + assert mb_to_hpa(1050) == 1050, "value not correct" + assert mb_to_hpa(1013) == 1013, "value not correct" + assert mb_to_hpa(939) == 939, "value not correct" + assert mb_to_hpa(900) == 900, "value not correct" + + def test__mb_to_in32(self): + # make sure some hard-coded values work + assert round(mb_to_in32(1050), 4) == 31.0065, "value not correct" + assert round(mb_to_in32(1013), 4) == 29.9139, "value not correct" + assert round(mb_to_in32(939), 4) == 27.7287, "value not correct" + assert round(mb_to_in32(900), 4) == 26.5770, "value not correct" + + def test__mb_to_in60(self): + # make sure some hard-coded values work + assert round(mb_to_in60(1050), 4) == 31.0905, "value not correct" + assert round(mb_to_in60(1013), 4) == 29.9949, "value not correct" + assert round(mb_to_in60(939), 4) == 27.8038, "value not correct" + assert round(mb_to_in60(900), 4) == 26.6490, "value not correct" + + def test__mb_to_kpa(self): + # make sure some hard-coded values work + assert round(mb_to_kpa(1050), 4) == 105.0000, "value not correct" + assert round(mb_to_kpa(1013), 4) == 101.3000, "value not correct" + assert round(mb_to_kpa(939), 4) == 93.9000, "value not correct" + assert round(mb_to_kpa(900), 4) == 90.0000, "value not correct" + + def test__mb_to_mm32(self): + # make sure some hard-coded values work + assert round(mb_to_mm32(1050), 4) == 787.5630, "value not correct" + assert round(mb_to_mm32(1013), 4) == 759.8108, "value not correct" + assert round(mb_to_mm32(939), 4) == 704.3063, "value not correct" + assert round(mb_to_mm32(900), 4) == 675.0540, "value not correct" + + def test__mb_to_mm60(self): + # make sure some hard-coded values work + assert round(mb_to_mm60(1050), 4) == 789.7890, "value not correct" + assert round(mb_to_mm60(1013), 4) == 761.9583, "value not correct" + assert round(mb_to_mm60(939), 4) == 706.2970, "value not correct" + assert round(mb_to_mm60(900), 4) == 676.9620, "value not correct" + + def test__mb_to_n_sqm(self): + # make sure some hard-coded values work + assert mb_to_n_sqm(1050) == 105000, "value not correct" + assert mb_to_n_sqm(1013) == 101300, "value not correct" + assert mb_to_n_sqm(939) == 93900, "value not correct" + assert mb_to_n_sqm(900) == 90000, "value not correct" + + def test__mb_to_pa(self): + # make sure some hard-coded values work + assert mb_to_pa(1050) == 105000, "value not correct" + assert mb_to_pa(1013) == 101300, "value not correct" + assert mb_to_pa(939) == 93900, "value not correct" + assert mb_to_pa(900) == 90000, "value not correct" + + def test__mb_to_lb_sqft(self): + # make sure some hard-coded values work + assert round(mb_to_lb_sqft(1050), 4) == 2192.9702, "value not correct" + assert round(mb_to_lb_sqft(1013), 4) == 2115.6941, "value not correct" + assert round(mb_to_lb_sqft(939), 4) == 1961.1419, "value not correct" + assert round(mb_to_lb_sqft(900), 4) == 1879.6887, "value not correct" + + def test__mb_to_lb_sqin(self): + # make sure some hard-coded values work + assert round(mb_to_lb_sqin(1050), 4) == 15.2290, "value not correct" + assert round(mb_to_lb_sqin(1013), 4) == 14.6923, "value not correct" + assert round(mb_to_lb_sqin(939), 4) == 13.6191, "value not correct" + assert round(mb_to_lb_sqin(900), 4) == 13.0534, "value not correct" + + def test__mm32_to_mb(self): + # make sure some hard-coded values work + assert round(mm32_to_mb(787), 4) == 1049.2441, "value not correct" + assert round(mm32_to_mb(759), 4) == 1011.9140, "value not correct" + assert round(mm32_to_mb(704), 4) == 938.5869, "value not correct" + assert round(mm32_to_mb(675), 4) == 899.9235, "value not correct" + + def test__mm60_to_mb(self): + # make sure some hard-coded values work + for p, expected in [(787, 1046.2929), + (759, 1009.0677), + (704, 935.9469), + (675, 897.3923)]: + self.assertAlmostEqual( + mm60_to_mb(p), + expected, + places=4, + msg=f"Incorrect conversion for {p}") + + def test__n_sqm_to_mb(self): + # make sure some hard-coded values work + assert n_sqm_to_mb(105000) == 1050, "value not correct" + assert n_sqm_to_mb(101300) == 1013, "value not correct" + assert n_sqm_to_mb(93900) == 939, "value not correct" + assert n_sqm_to_mb(90000) == 900, "value not correct" + + def test__pa_to_atm(self): + # make sure some hard-coded values work + assert round(pa_to_atm(105000), 4) == 1.0362, "value not correct" + assert round(pa_to_atm(101300), 4) == 0.9997, "value not correct" + assert round(pa_to_atm(93900), 4) == 0.9267, "value not correct" + assert round(pa_to_atm(90000), 4) == 0.8882, "value not correct" + + def test__pa_to_mb(self): + # make sure some hard-coded values work + assert pa_to_mb(105000) == 1050, "value not correct" + assert pa_to_mb(101300) == 1013, "value not correct" + assert pa_to_mb(93900) == 939, "value not correct" + assert pa_to_mb(90000) == 900, "value not correct" + + def test__hpa_to_mb(self): + # make sure some hard-coded values work + assert hpa_to_mb(1050) == 1050, "value not correct" + assert hpa_to_mb(1013) == 1013, "value not correct" + assert hpa_to_mb(939) == 939, "value not correct" + assert hpa_to_mb(900) == 900, "value not correct" + + def test__kpa_to_mb(self): + # make sure some hard-coded values work + assert kpa_to_mb(1050) == 10500, "value not correct" + assert kpa_to_mb(1013) == 10130, "value not correct" + assert kpa_to_mb(939) == 9390, "value not correct" + assert kpa_to_mb(900) == 9000, "value not correct" + + def test__lb_sqft_to_mb(self): + # make sure some hard-coded values work + assert round(lb_sqft_to_mb(2192), 4) == 1049.5362, "value not correct" + assert round(lb_sqft_to_mb(2115), 4) == 1012.6683, "value not correct" + assert round(lb_sqft_to_mb(1961), 4) == 938.9327, "value not correct" + assert round(lb_sqft_to_mb(1879), 4) == 899.6708, "value not correct" + + def test__lb_sqin_to_atm(self): + # make sure some hard-coded values work + assert round(lb_sqin_to_atm(15.2310), 4) == 1.0364, "value not correct" + assert round(lb_sqin_to_atm(14.6960), 4) == 1.0000, "value not correct" + assert round(lb_sqin_to_atm(13.6203), 4) == 0.9268, "value not correct" + assert round(lb_sqin_to_atm(13.0545), 4) == 0.8883, "value not correct" + + def test__lb_sqin_to_mm32(self): + # make sure some hard-coded values work + assert round(lb_sqin_to_mm32(15.2310), 4) == 31.0106, \ + "value not correct" + assert round(lb_sqin_to_mm32(14.6960), 4) == 29.9213, \ + "value not correct" + assert round(lb_sqin_to_mm32(13.6203), 4) == 27.7312, \ + "value not correct" + assert round(lb_sqin_to_mm32(13.0545), 4) == 26.5792, \ + "value not correct" + + def test__lb_sqin_to_mm60(self): + # make sure some hard-coded values work + assert round(lb_sqin_to_mm60(15.2310), 4) == 31.0982, \ + "value not correct" + assert round(lb_sqin_to_mm60(14.6960), 4) == 30.0059, \ + "value not correct" + assert round(lb_sqin_to_mm60(13.6203), 4) == 27.8095, \ + "value not correct" + assert round(lb_sqin_to_mm60(13.0545), 4) == 26.6543, \ + "value not correct" + + def test__lb_sqin_to_mb(self): + # make sure some hard-coded values work + assert round(lb_sqin_to_mb(15.2310), 4) == 1050.1386, \ + "value not correct" + assert round(lb_sqin_to_mb(14.6960), 4) == 1013.2517, \ + "value not correct" + assert round(lb_sqin_to_mb(13.6203), 4) == 939.0849, \ + "value not correct" + assert round(lb_sqin_to_mb(13.0545), 4) == 900.0745, \ + "value not correct" + + +def main(): + suite = unittest.makeSuite(TestCase, 'test') + runner = unittest.TextTestRunner() + runner.run(suite) + + +if __name__ == '__main__': + main() diff --git a/davisAPI/PyWeather-master/weather/units/tests/test_temp.py b/davisAPI/PyWeather-master/weather/units/tests/test_temp.py new file mode 100644 index 0000000..2ce555e --- /dev/null +++ b/davisAPI/PyWeather-master/weather/units/tests/test_temp.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python + +# +# Unit Tests for temp module +# +# See __usage__ for an explanation of runtime arguments. +# +# -Christopher Blunck +# + + + +import unittest + +from ..temp import * + +__author__ = 'Christopher Blunck' +__email__ = 'chris@wxnet.org' +__revision__ = '$Revision: 1.6 $' + +__doc__ = ''' +Unit tests the temp module. +''' + +__usage__ = ''' + python $0 +''' + + +def usage(): + print(__usage__) + sys.exit(1) + + +class TestCase(unittest.TestCase): + def setUp(self): pass + + def tearDown(self): pass + + def test__calc_heat_index(self): + # if the temperature is < 80, heat index == temperature + assert calc_heat_index(70, 100) == 70 , "value not correct" + assert calc_heat_index(79.9, 100) == 79.9 , "value not correct" + assert calc_heat_index(80, 100) != 80 , "value not correct" + + # make sure some hard-coded values work + assert int(calc_heat_index(80, 100)) == 87, "value not correct" + assert int(calc_heat_index(80, 10)) == 78, "value not correct" + assert int(calc_heat_index(90, 50)) == 94, "value not correct" + assert int(calc_heat_index(120, 100)) == 380, "value not correct" + + + def test__calc_wind_chill(self): + # make sure some hard-coded values work + assert int(calc_wind_chill(80, 10)) == 83, "value not correct" + assert int(calc_wind_chill(32, 10)) == 23, "value not correct" + assert int(calc_wind_chill(-20, 5)) == -34, "value not correct" + + + def test__fahrenheit_to_celsius(self): + # make sure some special values work + assert int(fahrenheit_to_celsius(32)) == 0, "value not correct" + assert int(fahrenheit_to_celsius(212)) == 100, "value not correct" + + # make sure some hard coded values work + assert int(fahrenheit_to_celsius(60)) == 15, "value not correct" + assert int(fahrenheit_to_celsius(-60)) == -51, "value not correct" + assert int(fahrenheit_to_celsius(90)) == 32, "value not correct" + + + def test__celsius_to_fahrenheit(self): + # make sure some special values work + assert int(celsius_to_fahrenheit(0)) == 32, "value not correct" + assert int(celsius_to_fahrenheit(100)) == 212, "value not correct" + + # make sure some hard coded values work + assert int(celsius_to_fahrenheit(60)) == 140, "value not correct" + assert int(celsius_to_fahrenheit(-60)) == -76, "value not correct" + assert int(celsius_to_fahrenheit(30)) == 86, "value not correct" + + + def test__celsius_to_kelvin(self): + # make sure some special values work + assert int(celsius_to_kelvin(-273.15)) == 0, "value not correct" + assert int(celsius_to_kelvin(100)) == 373, "value not correct" + + # make sure some hard coded values work + assert int(celsius_to_kelvin(60)) == 333, "value not correct" + assert int(celsius_to_kelvin(-60)) == 213, "value not correct" + assert int(celsius_to_kelvin(30)) == 303, "value not correct" + + + def test__celsius_to_rankine(self): + # make sure some special values work + assert int(celsius_to_rankine(0)) == 491, "value not correct" + assert int(celsius_to_rankine(100)) == 671, "value not correct" + + # make sure some hard coded values work + assert int(celsius_to_rankine(60)) == 599, "value not correct" + assert int(celsius_to_rankine(-60)) == 383, "value not correct" + assert int(celsius_to_rankine(30)) == 545, "value not correct" + + + def test__fahrenheit_to_kelvin(self): + # make sure some special values work + assert int(fahrenheit_to_kelvin(32)) == 273, "value not correct" + assert int(fahrenheit_to_kelvin(212)) == 373, "value not correct" + + # make sure some hard coded values work + assert int(fahrenheit_to_kelvin(60)) == 288, "value not correct" + assert int(fahrenheit_to_kelvin(-60)) == 222, "value not correct" + assert int(fahrenheit_to_kelvin(90)) == 305, "value not correct" + + + def test__fahrenheit_to_rankine(self): + # make sure some special values work + assert int(fahrenheit_to_rankine(32)) == 491, "value not correct" + assert int(fahrenheit_to_rankine(212)) == 671, "value not correct" + + # make sure some hard coded values work + assert int(fahrenheit_to_rankine(60)) == 519, "value not correct" + assert int(fahrenheit_to_rankine(-60)) == 399, "value not correct" + assert int(fahrenheit_to_rankine(90)) == 549, "value not correct" + + + def test__kelvin_to_celsius(self): + # make sure some special values work + assert int(kelvin_to_celsius(273.15)) == 0, "value not correct" + assert int(kelvin_to_celsius(373.15)) == 100, "value not correct" + + # make sure some hard coded values work + assert int(kelvin_to_celsius(0)) == -273, "value not correct" + assert int(kelvin_to_celsius(293.15)) == 20, "value not correct" + assert int(kelvin_to_celsius(343.15)) == 70, "value not correct" + + + def test__kelvin_to_fahrenheit(self): + # make sure some special values work + assert int(kelvin_to_fahrenheit(273.15)) == 32, "value not correct" + assert int(kelvin_to_fahrenheit(373.15)) == 212, "value not correct" + + # make sure some hard coded values work + assert int(kelvin_to_fahrenheit(0)) == -459, "value not correct" + assert int(kelvin_to_fahrenheit(293.15)) == 68, "value not correct" + assert int(kelvin_to_fahrenheit(343.15)) == 158, "value not correct" + + + def test__kelvin_to_rankine(self): + # make sure some special values work + assert int(kelvin_to_rankine(273.15)) == 491, "value not correct" + assert int(kelvin_to_rankine(373.15)) == 671, "value not correct" + + # make sure some hard coded values work + assert int(kelvin_to_rankine(0)) == 0, "value not correct" + assert int(kelvin_to_rankine(293.15)) == 527, "value not correct" + assert int(kelvin_to_rankine(343.15)) == 617, "value not correct" + + + def test__rankine_to_celsius(self): + # make sure some special values work + assert int(rankine_to_celsius(491)) == 0, "value not correct" + assert int(rankine_to_celsius(671)) == 99, "value not correct" + + # make sure some hard coded values work + assert int(rankine_to_celsius(0)) == -273, "value not correct" + assert int(rankine_to_celsius(527)) == 19, "value not correct" + assert int(rankine_to_celsius(617)) == 69, "value not correct" + + + def test__rankine_to_fahrenheit(self): + # make sure some special values work + assert int(rankine_to_fahrenheit(491)) == 31, "value not correct" + assert int(rankine_to_fahrenheit(671)) == 211, "value not correct" + + # make sure some hard coded values work + assert int(rankine_to_fahrenheit(0)) == -459, "value not correct" + assert int(rankine_to_fahrenheit(527)) == 67, "value not correct" + assert int(rankine_to_fahrenheit(617)) == 157, "value not correct" + + + def test__rankine_to_kelvin(self): + # make sure some special values work + assert int(rankine_to_kelvin(491)) == 272, "value not correct" + assert int(rankine_to_kelvin(671)) == 372, "value not correct" + + # make sure some hard coded values work + assert int(rankine_to_kelvin(0)) == 0, "value not correct" + assert int(rankine_to_kelvin(527)) == 292, "value not correct" + assert int(rankine_to_kelvin(617)) == 342, "value not correct" + + + def test__dewpoint(self): + # make sure some hard coded values work + assert int(calc_dewpoint(12, 72)) == 4, "value not correct" + assert int(calc_dewpoint(75, 33)) == 43, "value not correct" + assert int(calc_dewpoint(90, 85)) == 84, "value not correct" + + def test__humidity(self): + # make sure some hard coded values work + assert int(calc_humidity(87, 76) * 100) == 69, "value not correct" + assert int(calc_humidity(75, 45) * 100) == 34, "value not correct" + assert int(calc_humidity(50, 10) * 100) == 19, "value not correct" + assert int(calc_humidity(100, 88) * 100) == 68, "value not correct" + + +def main(): + suite = unittest.makeSuite(TestCase, 'test') + runner = unittest.TextTestRunner() + runner.run(suite) + +if __name__ == '__main__': + main() diff --git a/davisAPI/PyWeather-master/weather/units/tests/test_wind.py b/davisAPI/PyWeather-master/weather/units/tests/test_wind.py new file mode 100644 index 0000000..962fbaa --- /dev/null +++ b/davisAPI/PyWeather-master/weather/units/tests/test_wind.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python + +# +# Unit Tests for pressure module +# +# See __usage__ for an explanation of runtime arguments. +# +# -Christopher Blunck +# + + + +import unittest + +from ..wind import * + +__author__ = 'Christopher Blunck' +__email__ = 'chris@wxnet.org' +__revision__ = '$Revision: 1.6 $' + +__doc__ = ''' +Unit tests the pressure module. +''' + +__usage__ = ''' + python $0 +''' + + +def usage(): + print(__usage__) + sys.exit(1) + + +class TestCase(unittest.TestCase): + def setUp(self): pass + + def tearDown(self): pass + + + def test__knots_to_ft_sec(self): + # make sure some hard-coded values work + assert round(knots_to_ft_sec(5), 4) == 8.4390, "value not correct" + assert round(knots_to_ft_sec(14), 4) == 23.6293, "value not correct" + assert round(knots_to_ft_sec(35), 4) == 59.0733, "value not correct" + assert round(knots_to_ft_sec(70), 4) == 118.1467, "value not correct" + + + def test__knots_to_km_hr(self): + # make sure some hard-coded values work + assert round(knots_to_km_hr(5), 4) == 9.2600, "value not correct" + assert round(knots_to_km_hr(14), 4) == 25.9280, "value not correct" + assert round(knots_to_km_hr(35), 4) == 64.8200, "value not correct" + assert round(knots_to_km_hr(70), 4) == 129.6400, "value not correct" + + + def test__knots_to_m_sec(self): + # make sure some hard-coded values work + assert round(knots_to_m_sec(5), 4) == 2.5722, "value not correct" + assert round(knots_to_m_sec(14), 4) == 7.2022, "value not correct" + assert round(knots_to_m_sec(35), 4) == 18.0055, "value not correct" + assert round(knots_to_m_sec(70), 4) == 36.0111, "value not correct" + + + def test__knots_to_mph(self): + # make sure some hard-coded values work + assert round(knots_to_mph(5), 4) == 5.7539, "value not correct" + assert round(knots_to_mph(14), 4) == 16.1109, "value not correct" + assert round(knots_to_mph(35), 4) == 40.2773, "value not correct" + assert round(knots_to_mph(70), 4) == 80.5546, "value not correct" + + + def test__knots_to_nmph(self): + # make sure some hard-coded values work + assert round(knots_to_nmph(5), 4) == 5, "value not correct" + assert round(knots_to_nmph(14), 4) == 14, "value not correct" + assert round(knots_to_nmph(35), 4) == 35, "value not correct" + assert round(knots_to_nmph(70), 4) == 70, "value not correct" + + + def test__ft_sec_to_knots(self): + # make sure some hard-coded values work + assert round(ft_sec_to_knots(8.4390), 4) == 5, "value not correct" + assert round(ft_sec_to_knots(23.6293), 4) == 14, "value not correct" + assert round(ft_sec_to_knots(59.0733), 4) == 35, "value not correct" + assert round(ft_sec_to_knots(118.1467), 4) == 70, "value not correct" + + + def test__km_hr_to_knots(self): + # make sure some hard-coded values work + assert round(km_hr_to_knots(9.2600), 4) == 5, "value not correct" + assert round(km_hr_to_knots(25.9280), 4) == 14, "value not correct" + assert round(km_hr_to_knots(64.8200), 4) == 35, "value not correct" + assert round(km_hr_to_knots(129.6400), 4) == 70, "value not correct" + + + def test__m_sec_to_knots(self): + # make sure some hard-coded values work + assert round(m_sec_to_knots(2.5722), 4) == 5, "value not correct" + assert round(m_sec_to_knots(7.2022), 4) == 14, "value not correct" + assert round(m_sec_to_knots(18.0055), 4) == 34.9999, "value not correct" + assert round(m_sec_to_knots(36.0111), 4) == 70, "value not correct" + + + def test__mph_to_knots(self): + # make sure some hard-coded values work + assert round(mph_to_knots(5.7539), 4) == 5, "value not correct" + assert round(mph_to_knots(16.1109), 4) == 14, "value not correct" + assert round(mph_to_knots(40.2773), 4) == 35, "value not correct" + assert round(mph_to_knots(80.5546), 4) == 70, "value not correct" + + + def test__nmph_to_knots(self): + # make sure some hard-coded values work + assert round(nmph_to_knots(5), 4) == 5, "value not correct" + assert round(nmph_to_knots(14), 4) == 14, "value not correct" + assert round(nmph_to_knots(35), 4) == 35, "value not correct" + assert round(nmph_to_knots(70), 4) == 70, "value not correct" + + + def test__mph_to_ft_min(self): + # make sure some hard-coded values work + assert round(mph_to_ft_min(5), 4) == 440, "value not correct" + assert round(mph_to_ft_min(16), 4) == 1408, "value not correct" + assert round(mph_to_ft_min(40), 4) == 3520, "value not correct" + assert round(mph_to_ft_min(80), 4) == 7040, "value not correct" + + + def test__mph_to_ft_sec(self): + # make sure some hard-coded values work + assert round(mph_to_ft_sec(5), 4) == 7.3333, "value not correct" + assert round(mph_to_ft_sec(16), 4) == 23.4667, "value not correct" + assert round(mph_to_ft_sec(40), 4) == 58.6666, "value not correct" + assert round(mph_to_ft_sec(80), 4) == 117.3333, "value not correct" + + + def test__mph_to_km_hr(self): + # make sure some hard-coded values work + assert round(mph_to_km_hr(5), 4) == 8.0467, "value not correct" + assert round(mph_to_km_hr(16), 4) == 25.7495, "value not correct" + assert round(mph_to_km_hr(40), 4) == 64.3738, "value not correct" + assert round(mph_to_km_hr(80), 4) == 128.7475, "value not correct" + + + def test__mph_to_m_sec(self): + # make sure some hard-coded values work + assert round(mph_to_m_sec(5), 4) == 2.2352, "value not correct" + assert round(mph_to_m_sec(16), 4) == 7.1526, "value not correct" + assert round(mph_to_m_sec(40), 4) == 17.8816, "value not correct" + assert round(mph_to_m_sec(80), 4) == 35.7632, "value not correct" + + + def test__ft_min_to_mph(self): + # make sure some hard-coded values work + assert round(ft_min_to_mph(440), 4) == 5, "value not correct" + assert round(ft_min_to_mph(1408), 4) == 16, "value not correct" + assert round(ft_min_to_mph(3520), 4) == 40, "value not correct" + assert round(ft_min_to_mph(7040), 4) == 80, "value not correct" + + + def test__ft_sec_to_knots(self): + # make sure some hard-coded values work + assert round(ft_sec_to_knots(8.4390), 4) == 5, "value not correct" + assert round(ft_sec_to_knots(23.6293), 4) == 14, "value not correct" + assert round(ft_sec_to_knots(59.0733), 4) == 35, "value not correct" + assert round(ft_sec_to_knots(118.1467), 4) == 70, "value not correct" + + + def test__ft_sec_to_mph(self): + # make sure some hard-coded values work + assert round(ft_sec_to_mph(7.3333), 4) == 5, "value not correct" + assert round(ft_sec_to_mph(23.4667), 4) == 16, "value not correct" + assert round(ft_sec_to_mph(58.6666), 4) == 39.9999, "value not correct" + assert round(ft_sec_to_mph(117.3333), 4) == 80, "value not correct" + + + def test__km_hr_to_knots(self): + # make sure some hard-coded values work + assert round(km_hr_to_knots(9.2600), 4) == 5, "value not correct" + assert round(km_hr_to_knots(25.9280), 4) == 14, "value not correct" + assert round(km_hr_to_knots(64.8200), 4) == 35, "value not correct" + assert round(km_hr_to_knots(129.6400), 4) == 70, "value not correct" + + + def test__km_hr_to_mph(self): + # make sure some hard-coded values work + assert round(km_hr_to_mph(8.0467), 4) == 5, "value not correct" + assert round(km_hr_to_mph(25.7495), 4) == 16, "value not correct" + assert round(km_hr_to_mph(64.3738), 4) == 40, "value not correct" + assert round(km_hr_to_mph(128.7475), 4) == 80, "value not correct" + + + def test__m_sec_to_knots(self): + # make sure some hard-coded values work + assert round(m_sec_to_knots(2.5722), 4) == 5, "value not correct" + assert round(m_sec_to_knots(7.2022), 4) == 14, "value not correct" + assert round(m_sec_to_knots(18.0055), 4) == 34.9999, "value not correct" + assert round(m_sec_to_knots(36.0111), 4) == 70, "value not correct" + + + def test__m_sec_to_mph(self): + # make sure some hard-coded values work + assert round(m_sec_to_mph(2.2352), 4) == 5, "value not correct" + assert round(m_sec_to_mph(7.1526), 4) == 15.9999, "value not correct" + assert round(m_sec_to_mph(17.8816), 4) == 40, "value not correct" + assert round(m_sec_to_mph(35.7632), 4) == 80, "value not correct" + + +def main(): + suite = unittest.makeSuite(TestCase, 'test') + runner = unittest.TextTestRunner() + runner.run(suite) + +if __name__ == '__main__': + main() diff --git a/davisAPI/PyWeather-master/weather/units/wind.py b/davisAPI/PyWeather-master/weather/units/wind.py new file mode 100644 index 0000000..b0b66ab --- /dev/null +++ b/davisAPI/PyWeather-master/weather/units/wind.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python + +# +# See __doc__ for an explanation of what this module does +# +# See __usage__ for an explanation of runtime arguments. +# +# -Christopher Blunck +# + +import sys + +__author__ = 'Christopher Blunck' +__email__ = 'chris@wxnet.org' +__revision__ = '$Revision: 1.6 $' + +__doc__ = 'wind related conversion functions' +__usage__ = 'this module should not be run via the command line' + + +def knots_to_ft_sec(kts): + """Knots (kt) to feet/second (ft/s)""" + return kts * 1.6878099 + + +def knots_to_km_hr(kts): + """Knots (kt) to kilometers/hour (kph)""" + return kts * 1.852 + + +def knots_to_m_sec(kts): + """Knots (kt) to meters/second (m/s)""" + return kts * 0.514444 + + +def knots_to_mph(kts): + """Knots (kt) to miles/hour (mph)""" + return kts * 1.1507794 + + +def knots_to_nmph(kts): + """Knots (kt) to nautical miles/hour (nmph)""" + return kts + + +def ft_sec_to_knots(ft): + """Feet/second (ft/s) to knots (kt)""" + return ft * 0.5924838 + + +def km_hr_to_knots(km): + """Kilometers/hour (kph) to knots (kt)""" + return km * 0.5399568 + + +def m_sec_to_knots(m): + """Meters/second (m/s) to knots (kt)""" + return m * 1.943846 + + +def mph_to_knots(mph): + """Miles/hour (mph) to knots (kt)""" + return mph * 0.86897624 + + +def nmph_to_knots(mph): + """Nautical miles/hour (nmph) to knots (kt)""" + return mph + + +def mph_to_ft_min(mph): + """Miles/hour (mph) to feet/minute (ft/min)""" + return mph * 88 + + +def mph_to_ft_sec(mph): + """Miles/hour (mph) to feet/second (ft/s)""" + return mph * 1.466666 + + +def mph_to_km_hr(mph): + """Miles/hour (mph) to kilometers/hour (kph)""" + return mph * 1.609344 + + +def mph_to_m_sec(mph): + """Miles/hour (mph) to meters/second (m/s)""" + return mph * 0.44704 + + +def ft_min_to_mph(ft): + """Feet/minute (ft/min) to miles/hour (mph)""" + return ft * 0.01136363 + + +def ft_sec_to_knots(ft): + """Feet/second (ft/s) to knots (kt)""" + return ft * 0.5924838 + + +def ft_sec_to_mph(ft): + """Feet/second (ft/s) to miles/hour (mph)""" + return ft * 0.681818 + + +def km_hr_to_knots(km): + """Kilometers/hour (kph) to knots (kt)""" + return km * 0.5399568 + + +def km_hr_to_mph(km): + """Kilometers/hour (kph) to miles/hour (mph)""" + return km * 0.62137119 + + +def m_sec_to_knots(m): + """Meters/second (m/s) to knots (kt)""" + return m * 1.943846 + + +def m_sec_to_mph(m): + """Meters/second (m/s) to miles/hour (mph)""" + return m * 2.2369363 diff --git a/davisAPI/davisAPI.py b/davisAPI/davisAPI.py new file mode 100644 index 0000000..c294ff9 --- /dev/null +++ b/davisAPI/davisAPI.py @@ -0,0 +1,77 @@ +from influxdb_client import InfluxDBClient, Point +from influxdb_client.client.write_api import SYNCHRONOUS +from weather.stations.davis import VantagePro +import gc +from pprint import pprint +import time +import logging +import serial.tools.list_ports +import os +import sys + + +logging.basicConfig(filename="Stations.log", + format='%(asctime)s %(message)s', + filemode='a') +logger = logging.getLogger('davis_api') +logger.setLevel(logging.DEBUG) + + + +def write_data(device, station, send=True): + try: + device.parse() + data = device.fields + points = [] + fields = ['BarTrend', 'CRC', 'DateStamp', 'DewPoint', 'HeatIndex', 'ETDay', 'HeatIndex', + 'HumIn', 'HumOut', 'Pressure', 'RainDay', 'RainMonth', 'RainRate', 'RainStorm', + 'RainYear', 'SunRise', 'SunSet', 'TempIn', 'TempOut', 'WindDir', 'WindSpeed', + 'WindSpeed10Min'] + if send: + for field in fields: + points.append(Point(station).field(field, data[field])) + write_api.write(bucket=bucket, record=points) + else: + pprint(data) + # tables = query_api.query(f'from(bucket:"{bucket}") |> range(start: -1h)') + del data + del points + del fields + gc.collect() + except Exception as e: + logger.error(str(e)) + raise e + +# убрать +try: + token = "2CEePET3ss2khtjsdGrJap8mVzHhR2dRwuyK3NuFBvDgGtOMSi6Jstsrp2o-OANzD8fxB73PsTyIbqgbnokoXQ==" + bucket = 'wind' + org = "UlSTU" + url = "http://influxdb.athene.tech/" + client = InfluxDBClient(url=url, token=token, org=org) + write_api = client.write_api(write_options=SYNCHRONOUS) + query_api = client.query_api() +except Exception as e: + logger.error('DB_ERR:' + str(e)) + raise e + +try: + ports = serial.tools.list_ports.comports() + available_ports = {} + for port in ports: +# print(port) + if port.serial_number == '0001': + available_ports['/dev/'+port.name] = port.vid + # COM = 'COM8' # '/dev/ttyUSB0' + # COM1 = '/dev/ttyUSB1' + # available_ports = {'/dev/ttyUSB0':'st1'} + devices = [VantagePro(port) for port in available_ports.keys()] + # print(available_ports) + while True: + for i in range(len(devices)): + write_data(devices[i], 'st' + str(available_ports[list(available_ports.keys())[i]]), True) + time.sleep(15) # лучше 60 +except Exception as e: + logger.error('Device_error' + str(e)) + raise e + diff --git a/davisAPI/davisAPI.py.save b/davisAPI/davisAPI.py.save new file mode 100644 index 0000000..fb0dd7a --- /dev/null +++ b/davisAPI/davisAPI.py.save @@ -0,0 +1,69 @@ +from influxdb_client import InfluxDBClient, Point +from influxdb_client.client.write_api import SYNCHRONOUS +from weather.stations.davis import VantagePro +import gc +from pprint import pprint +import time +import logging +import serial.tools.list_ports + +logging.basicConfig(filename="Stations.log", + format='%(asctime)s %(message)s', + filemode='a') +logger = logging.getLogger('davis_api') +logger.setLevel(logging.DEBUG) + + +def write_data(device, station, send=True): + try: + device.parse() + data = device.fields + points = [] + fields = ['BarTrend', 'CRC', 'DateStamp', 'DateStampUTC', 'DewPoint', 'HeatIndex', 'ETDay', 'HeatIndex', + 'HumIn', 'HumOut', 'LeafWetness', 'Pressure', 'RainDay', 'RainMonth', 'RainRate', 'RainStorm', + 'RainYear', 'SoilMoist', 'SunRise', 'SunSet', 'TempIn', 'TempOut', 'WindDir', 'WindSpeed', + 'WindSpeed10Min'] + if send: + for field in fields: + points.append(Point(station).field(field, data[field])) + write_api.write(bucket=bucket, record=points) + else: + pprint(data) + # tables = query_api.query(f'from(bucket:"{bucket}") |> range(start: -1h)') + del data + del points + del fields + gc.collect() + except Exception as e: + logger.error(str(e)) + + +try: + token = "2CEePET3ss2khtjsdGrJap8mVzHhR2dRwuyK3NuFBvDgGtOMSi6Jstsrp2o-OANzD8fxB73PsTyIbqgbnokoXQ==" + bucket = 'wind' + org = "UlSTU" + url = "http://influxdb.athene.tech/" + client = InfluxDBClient(url=url, token=token, org=org) + write_api = client.write_api(write_options=SYNCHRONOUS) + query_api = client.query_api() +except Exception as e: + logger.error('DB_ERR:' + str(e)) + +try: + ports = serial.tools.list_ports.comports() + available_ports = {} + for port in ports: + print(port) + if port.manufacturer == 'Silicon Labs': + available_ports['/dev/'+port.name] = port.vid + # COM = 'COM8' # '/dev/ttyUSB0' + # COM1 = '/dev/ttyUSB1 +' + devices = [VantagePro(port) for port in available_ports.keys()] + print(available_ports) + while True: + for i in range(len(devices)): + write_data(devices[i], list(available_ports.keys())[i], False) + time.sleep(15) +except Exception as e: + logger.error('Device_error' + str(e)) diff --git a/davisAPI/davisAPI.py.save.1 b/davisAPI/davisAPI.py.save.1 new file mode 100644 index 0000000..be4d479 --- /dev/null +++ b/davisAPI/davisAPI.py.save.1 @@ -0,0 +1,69 @@ +from influxdb_client import InfluxDBClient, Point +from influxdb_client.client.write_api import SYNCHRONOUS +from weather.stations.davis import VantagePro +import gc +from pprint import pprint +import time +import logging +import serial.tools.list_ports + +logging.basicConfig(filename="Stations.log", + format='%(asctime)s %(message)s', + filemode='a') +logger = logging.getLogger('davis_api') +logger.setLevel(logging.DEBUG) + + +def write_data(device, station, send=True): + try: + device.parse() + data = device.fields + points = [] + fields = ['BarTrend', 'CRC', 'DateStamp', 'DateStampUTC', 'DewPoint', 'HeatIndex', 'ETDay', 'HeatIndex', + 'HumIn', 'HumOut', 'LeafWetness', 'Pressure', 'RainDay', 'RainMonth', 'RainRate', 'RainStorm', + 'RainYear', 'SoilMoist', 'SunRise', 'SunSet', 'TempIn', 'TempOut', 'WindDir', 'WindSpeed', + 'WindSpeed10Min'] + if send: + for field in fields: + points.append(Point(station).field(field, data[field])) + write_api.write(bucket=bucket, record=points) + else: + pprint(data) + # tables = query_api.query(f'from(bucket:"{bucket}") |> range(start: -1h)') + del data + del points + del fields + gc.collect() + except Exception as e: + logger.error(str(e)) + + +try: + token = "2CEePET3ss2khtjsdGrJap8mVzHhR2dRwuyK3NuFBvDgGtOMSi6Jstsrp2o-OANzD8fxB73PsTyIbqgbnokoXQ==" + bucket = 'wind' + org = "UlSTU" + url = "http://influxdb.athene.tech/" + client = InfluxDBClient(url=url, token=token, org=org) + write_api = client.write_api(write_options=SYNCHRONOUS) + query_api = client.query_api() +except Exception as e: + logger.error('DB_ERR:' + str(e)) + +try: + ports = serial.tools.list_ports.comports() + available_ports = {} + for port in ports: + if port.manufacturer == 'Silicon Labs': + available_ports['/dev/' + port.name] = port.vid + # COM = 'COM8' # '/dev/ttyUSB0' + # COM1 = '/dev/ttyUSB1' + print(ports) + print(available_ports) + device = VantagePro(available_ports + devices = [VantagePro(port) for port in available_ports.keys()] + while True: + for i in range(len(devices)): + write_data(devices[i], list(available_ports.keys())[i], False) + time.sleep(15) +except Exception as e: + logger.error('Device_error' + str(e)) diff --git a/davisAPI/davisAPI.py.save.2 b/davisAPI/davisAPI.py.save.2 new file mode 100644 index 0000000..0c0fb99 --- /dev/null +++ b/davisAPI/davisAPI.py.save.2 @@ -0,0 +1,82 @@ +from influxdb_client import InfluxDBClient, Point +from influxdb_client.client.write_api import SYNCHRONOUS +from weather.stations.davis import VantagePro +import gc +from pprint import pprint +import time +import logging +import serial.tools.list_ports +import os +import sys +import psutil + +logging.basicConfig(filename="Stations.log", + format='%(asctime)s %(message)s', + filemode='a') +logger = logging.getLogger('davis_api') +logger.setLevel(logging.DEBUG) +ef restart_program(): + try: + p = psutil.Process(os.getpid()) + for handler in p.open_files() + p.connections(): + os.close(handler.fd) + except Exception as e: + logging.error(e) + python = sys.executable + os.execl(python,python,*sys.argv) + + +def write_data(device, station, send=True): + try: + device.parse() + data = device.fields + points = [] + fields = ['BarTrend', 'CRC', 'DateStamp', 'DewPoint', 'HeatIndex', 'ETDay', 'HeatIndex', + 'HumIn', 'HumOut', 'Pressure', 'RainDay', 'RainMonth', 'RainRate', 'RainStorm', + 'RainYear', 'SunRise', 'SunSet', 'TempIn', 'TempOut', 'WindDir', 'WindSpeed', + 'WindSpeed10Min'] + if send: + for field in fields: + points.append(Point(station).field(field, data[field])) + write_api.write(bucket=bucket, record=points) + else: + pprint(data) + # tables = query_api.query(f'from(bucket:"{bucket}") |> range(start: -1h)') + del data + del points + del fields + gc.collect() + except Exception as e: + logger.error(str(e)) + os.execl('runme.sh','') + + +try: + token = "2CEePET3ss2khtjsdGrJap8mVzHhR2dRwuyK3NuFBvDgGtOMSi6Jstsrp2o-OANzD8fxB73PsTyIbqgbnokoXQ==" + bucket = 'wind' + org = "UlSTU" + url = "http://influxdb.athene.tech/" + client = InfluxDBClient(url=url, token=token, org=org) + write_api = client.write_api(write_options=SYNCHRONOUS) + query_api = client.query_api() +except Exception as e: + logger.error('DB_ERR:' + str(e)) + +try: + ports = serial.tools.list_ports.comports() + available_ports = {} + for port in ports: +# print(port) + if port.serial_number == '0001': + available_ports['/dev/'+port.name] = port.vid + # COM = 'COM8' # '/dev/ttyUSB0' + # COM1 = '/dev/ttyUSB1' + # available_ports = {'/dev/ttyUSB0':'st1'} + devices = [VantagePro(port) for port in available_ports.keys()] + # print(available_ports) + while True: + for i in range(len(devices)): + write_data(devices[i], 'st' + str(available_ports[list(available_ports.keys())[i]]), True) + time.sleep(15) +except Exception as e: + logger.error('Device_error' + str(e)) diff --git a/davisAPI/get_ports.py b/davisAPI/get_ports.py new file mode 100644 index 0000000..c09a052 --- /dev/null +++ b/davisAPI/get_ports.py @@ -0,0 +1,15 @@ +import serial.tools.list_ports + +ports = serial.tools.list_ports.comports() + +for port, desc, hwid in sorted(ports): + print("{}: {} [{}]".format(port, desc, hwid)) + +available_ports = {} +for port in ports: + print(port.serial_number) + available_ports['/dev/' + port.name] = port.vid +devices = [port for port in available_ports.keys()] +print(devices) +print(ports[1].manufacturer) +#print(ports[0].name) diff --git a/davisAPI/get_ports.py.save b/davisAPI/get_ports.py.save new file mode 100644 index 0000000..0a7b03f --- /dev/null +++ b/davisAPI/get_ports.py.save @@ -0,0 +1,15 @@ +import serial.tools.list_ports + +ports = serial.tools.list_ports.comports() + +for port, desc, hwid in sorted(ports): + print("{}: {} [{}]".format(port, desc, hwid)) + +available_ports = {} +for port in ports: + print(port.serial_number) + available_ports['/dev/' + port.name] = port.vid +devices = [port for port in available_ports.keys()] +print(devices) +print(ports[1].manufacturer) +#print(ports[0].name) diff --git a/davisAPI/runme.sh b/davisAPI/runme.sh new file mode 100644 index 0000000..d1f29ee --- /dev/null +++ b/davisAPI/runme.sh @@ -0,0 +1,3 @@ +#!/bin/bash +kill -9 davisAPI.py +sudo python3 davisAPI.py diff --git a/davisAPI/runner.py b/davisAPI/runner.py new file mode 100644 index 0000000..2a94e59 --- /dev/null +++ b/davisAPI/runner.py @@ -0,0 +1,9 @@ +from subprocess import Popen +import sys + +#filename = sys.argv[1] +filename = "davisAPI.py" +while True: + print('Start'+filename) + p = Popen("python " + filename, shell=True) + p.wait() diff --git a/davisAPI/ulstu_influx.py b/davisAPI/ulstu_influx.py new file mode 100644 index 0000000..84f81ed --- /dev/null +++ b/davisAPI/ulstu_influx.py @@ -0,0 +1,27 @@ +from influxdb_client import InfluxDBClient, Point +from influxdb_client.client.write_api import SYNCHRONOUS + +bucket = "wind" + +client = InfluxDBClient(url="http://influxdb.athene.tech/", + token="2CEePET3ss2khtjsdGrJap8mVzHhR2dRwuyK3NuFBvDgGtOMSi6Jstsrp2o-OANzD8fxB73PsTyIbqgbnokoXQ==", + org="UlSTU") + +write_api = client.write_api(write_options=SYNCHRONOUS) +query_api = client.query_api() + +# p = Point("my_measurement").tag("location", "Prague").field("val", 200.0) +# +# write_api.write(bucket=bucket, record=p) + +tables = query_api.query('from(bucket:"wind") |> range(start: -7d)') + +for table in tables: + print(table) + for row in table.records: + print (row.values) + print(row.values['_field']) + print(row.values['_value']) + +# manger = InfluxManager("wind","http://localhost:8086","IkRV5NlnRp_fTHR5x4mgMzi_coQ31ILQBaQUf5wZfXIJ9iwCZBH9qiHnSREgdu_bdsmAjUisUTmqoMvpXDXUOA==","nikitaOrganization") +# print(manger.getDataByTime()) \ No newline at end of file