Initial import
authorTuomas Jormola <tj@solitudo.net>
Mon, 17 Jan 2011 21:39:05 +0000 (23:39 +0200)
committerTuomas Jormola <tj@solitudo.net>
Mon, 17 Jan 2011 21:39:05 +0000 (23:39 +0200)
COPYING [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.mdwn [new file with mode: 0644]
src/metar.lua [new file with mode: 0644]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..d511905
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  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
+this service 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 make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  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.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+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
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the 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 a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE 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.
+
+                    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
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 2 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, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision 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, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This 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.
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..2c6c0e8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,38 @@
+# Generate README.txt file and API documentation
+
+MDWN_FILES             = README.mdwn
+LUA_MAIN_MODULE        = $(LUA_SRC_DIR)/metar.lua
+SUBMODULES             = $(PWD)/submodules
+MDWN2TEXT_MK   = $(SUBMODULES)/mdwn2text/mdwn2text.mk
+LUADOC_DIR             = $(SUBMODULES)/luadoc-ikiwiki
+LUADOC_MK              = $(LUADOC_DIR)/luadoc.mk
+LUA_PATH        = ;;$(LUADOC_DIR)/src/?.lua
+
+export LUA_PATH
+
+all: update-readme update-luadoc
+
+include $(MDWN2TEXT_MK)
+include $(LUADOC_MK)
+
+update-readme: README.txt
+       git add README.txt
+       git commit -m'Updated README.txt from README.mdwn' README.txt || true
+
+update-luadoc: $(LUA_APIDOC_DIR)/html/index.html $(LUA_APIDOC_DIR)/ikiwiki/index.mdwn
+       git add $(LUA_APIDOC_DIR)
+       git commit -m'Updated API documentation' $(LUA_APIDOC_DIR) || true
+
+$(LUA_APIDOC_DIR)/html/index.html: $(LUA_MAIN_MODULE)
+       $(MAKE) luadoc-clean-html
+       $(MAKE) luadoc-html
+
+$(LUA_APIDOC_DIR)/ikiwiki/index.mdwn: $(LUA_MAIN_MODULE)
+       $(MAKE) luadoc-clean-ikiwiki
+       $(MAKE) luadoc-ikiwiki
+
+clean:
+       $(MAKE) luadoc-clean
+       rm -rf README.txt api
+
+.PHONY: all update-readme update-luadoc clean
diff --git a/README.mdwn b/README.mdwn
new file mode 100644 (file)
index 0000000..eb9a327
--- /dev/null
@@ -0,0 +1,57 @@
+# Lua METAR parser
+
+This library can be used to parse METAR coded weather reports
+and fetch current METAR reports from [NOAA](http://www.noaa.gov)
+[Internet Weather Service](http://weather.noaa.gov) in Lua.
+
+The parser is pretty simple and by no means claims to support every feature one
+might find in METAR coded weather reports. For example, weather forecasts and
+automatic weather reports are not detected. Unsupported features in
+the weather reports are silently dropped.
+
+# Dependencies
+
+[LuaSocket](http://www.cs.princeton.edu/~diego/professional/luasocket/) is
+required for fetching weather reports from the [Internet Weather Service](http://weather.noaa.gov).
+
+# Downloading
+
+METAR parser home page is at
+<http://solitudo.net/software/lua/metar/> and it can be downloaded
+from the public Git repository at `git://scm.solitudo.net/metar.git`.
+Gitweb interface is available at
+<http://scm.solitudo.net/gitweb/public/metar.git>.
+
+Remember to download the submodules after cloning `metar.git`.
+
+1. $ `git submodule init`
+1. $ `git submodule update`
+
+# Installation
+
+Install the `src/metar.lua` under some of the directories included in the
+default `LUA_PATH` for your Lua distribution, or install under desired location
+and set `LUA_PATH` accordingly. More info about `LUA_PATH` at
+<http://www.lua.org/pil/8.1.html>.
+
+# API Documentation
+
+API documentation for `metar` is available under the `api/` directory of
+the distribution.
+
+# References
+
+The following material was used when researching METAR
+
+* [Federal Meteorological Handbook No. 1 - Surface Weather Observations and Reports](http://www.ofcm.gov/fmh-1/fmh1.htm)
+* [Metar codes by Richard Ogley](http://www.astro.keele.ac.uk/oldusers/rno/Aviation/metar_codes.html)
+* [Meteorology @ West Moors - Metar decode](http://booty.org.uk/booty.weather/metinfo/codes/METAR_decode.htm)
+* Source code of [libgweather](http://ftp.gnome.org/pub/GNOME/sources/libgweather/)
+* Source code of [Metar](http://www.leune.org/metar/)
+
+# Copyright and licensing
+
+Copyright: © 2010 Tuomas Jormola <tj@solitudo.net> <http://solitudo.net>
+
+Licensed under the terms of the [GNU General Public License Version 2.0](http://www.gnu.org/licenses/gpl-2.0.html).
+License terms are included in the file `COPYING`.
diff --git a/src/metar.lua b/src/metar.lua
new file mode 100644 (file)
index 0000000..ec63d54
--- /dev/null
@@ -0,0 +1,745 @@
+-------------------------------------------------------------------------------
+-- Lua class to parse METAR coded weather reports and fetch current METAR
+-- reports from <a href="http://www.noaa.gov">NOAA</a> <a href="http://weather.noaa.gov">Internet Weather Service</a>.
+-- The parser is pretty simple and by no means claims to support every feature
+-- one might find in METAR coded weather reports. For example, weather forecasts
+-- and automatic weather reports are not detected. Unsupported features in
+-- the weather reports are silently dropped.
+-- @author Tuomas Jormola
+-- @copyright © 2010 Tuomas Jormola <a href="mailto:tj@solitudo.net">tj@solitudo.net</a> <a href="http://solitudo.net">http://solitudo.net</a>
+--  Licensed under the terms of
+-- the <a href="http://www.gnu.org/licenses/gpl-2.0.html">GNU General Public License Version 2.0</a>.
+--
+-------------------------------------------------------------------------------
+
+local socket_http  = require('socket.http')
+
+local weatherlib   = require('weatherlib')
+
+local os           = { date = os.date, time = os.time }
+local pairs        = pairs
+local print        = print
+local setmetatable = setmetatable
+local string       = { format = string.format }
+local table        = { insert = table.insert, maxn = table.maxn}
+local tonumber     = tonumber
+local type         = type
+
+module('metar')
+
+local token_data = {
+       wind = {
+               directions = {
+                       { key = 'N',   min = 349, max = 11                              },
+                       { key = 'NNE', min = 12,  max = 33                              },
+                       { key = 'NE',  min = 34,  max = 56                              },
+                       { key = 'ENE', min = 57,  max = 78                              },
+                       { key = 'E',   min = 79,  max = 101                             },
+                       { key = 'ESE', min = 102, max = 123                             },
+                       { key = 'SE',  min = 124, max = 146                             },
+                       { key = 'SSE', min = 147, max = 168                             },
+                       { key = 'S',   min = 169, max = 191                             },
+                       { key = 'SSW', min = 192, max = 213                             },
+                       { key = 'SW',  min = 214, max = 236                             },
+                       { key = 'WSW', min = 237, max = 258                             },
+                       { key = 'W',   min = 259, max = 281                             },
+                       { key = 'WNW', min = 282, max = 303                             },
+                       { key = 'NW',  min = 304, max = 326                             },
+                       { key = 'NNW', min = 327, max = 348                             },
+               },
+               speeds = {
+                       KT  = weatherlib.SPEED_UNITS.KNOT,
+                       MPS = weatherlib.SPEED_UNITS.MS,
+                       KMH = weatherlib.SPEED_UNITS.KMH,
+               },
+               offset = 1,
+       },
+       clouds = {
+               coverages = {
+                       { token = 'SKC', alias = 'CLR',   key = 'CLEAR'                 },
+                       { token = 'FEW',                  key = 'FEW'                   },
+                       { token = 'SCT',                  key = 'SCATTERED'             },
+                       { token = 'BKN',                  key = 'BROKEN_SKY'            },
+                       { token = 'OVC',                  key = 'OVERCAST'              },
+               },
+               types = {
+                       { token = 'CB',                   key = 'CUMULONIMBUS'          },
+                       { token = 'TCU',                  key = 'TOWERING_CUMULUS'      },
+               },
+       },
+       sky = {
+               types = {
+                       { token = 'CAVOK', alias = 'SKC', key = 'CLEAR'                 },
+                       { token = 'NSC',                  key = 'NO_SIGNIFICANT_CLOUDS' },
+                       { token = 'NCD',                  key = 'NO_CLOUDS_DETECTED'    },
+               },
+               offset = 3,
+       },
+       weather_intensity = {
+               types = {
+                       { token = '-',                    key = 'LIGHT'                 },
+                       { token = '+',                    key = 'HEAVY'                 },
+                       { token = 'VC',                   key = 'VICINITY'              },
+               },
+               offset = 1,
+       },
+       weather_descriptor = {
+               types = {
+                       { token = 'MI',                   key = 'SHALLOW'               },
+                       { token = 'PR',                   key = 'PARTIAL'               },
+                       { token = 'BC',                   key = 'PATCHES'               },
+                       { token = 'DR',                   key = 'DRIFTING'              },
+                       { token = 'BL',                   key = 'BLOWING'               },
+                       { token = 'SH',                   key = 'SHOWERS'               },
+                       { token = 'TS',                   key = 'THUNDERSTORM'          },
+                       { token = 'FZ',                   key = 'FREEZING'              },
+               },
+       },
+       weather_phenomena = {
+               types = {
+                       { token = 'DZ',                   key = 'DRIZZLE'               },
+                       { token = 'RA',                   key = 'RAIN'                  },
+                       { token = 'SN',                   key = 'SNOW'                  },
+                       { token = 'SG',                   key = 'SNOW_GRAINS'           },
+                       { token = 'IC',                   key = 'ICE_CRYSTALS'          },
+                       { token = 'PL', alias = 'PE',     key = 'ICE_PELLETS'           },
+                       { token = 'GR',                   key = 'HAIL'                  },
+                       { token = 'GS',                   key = 'SMALL_HAIL'            },
+                       { token = 'UP',                   key = 'UNKNOWN'               },
+                       { token = 'BR',                   key = 'MIST'                  },
+                       { token = 'FG',                   key = 'FOG'                   },
+                       { token = 'FU',                   key = 'SMOKE'                 },
+                       { token = 'VA',                   key = 'VOLCANIC_ASH'          },
+                       { token = 'DU',                   key = 'WIDESPREAD_DUST'       },
+                       { token = 'SA',                   key = 'SAND'                  },
+                       { token = 'HZ',                   key = 'HAZE'                  },
+                       { token = 'PY',                   key = 'SPRAY'                 },
+                       { token = 'PO',                   key = 'DUST_WHIRLS'           },
+                       { token = 'SQ',                   key = 'SQUALLS'               },
+                       { token = 'FC',                   key = 'FUNNEL_CLOUD'          },
+                       { token = 'SS',                   key = 'SAND_STORM'            },
+                       { token = 'DS',                   key = 'DUST_STORM'            },
+               },
+       }
+}
+
+-------------------------------------------------------------------------------
+-- Wind direction table.
+-- Values from this table are used in the result table with key <code>wind.direction</code>
+-- returned by <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
+-- @class table
+-- @name WIND_DIRECTION
+-- @field VRB Index value for variable speed direction
+-- @field N Index value for North
+-- @field NNE Index value for North - North East
+-- @field NE Index value for Nort - East
+-- @field ENE Index value for East - North East
+-- @field E Index value for East
+-- @field ESE Index value for East - South East
+-- @field SE Index value for South East
+-- @field SSE Index value for South - South Est
+-- @field S Index value for South
+-- @field SSW Index value for South - South West
+-- @field SW Index value for South West
+-- @field WSW Index value for West - South West
+-- @field W  Index value for West
+-- @field WNW Index value for West - North West
+-- @field NW Index value for North West
+-- @field NNW Index value for North - North West
+WIND_DIRECTION      = { VRB = 1 }
+
+-------------------------------------------------------------------------------
+-- Could coverage table.
+-- Values from this table are used in the result table with key <code>clouds[n].coverage</code> returned by
+-- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
+-- @class table
+-- @name CLOUD_COVERAGE
+-- @field CLEAR Clear
+-- @field FEW Few clouds
+-- @field SCATTERED Scattered clouds
+-- @field BROKEN_SKY Broken sky
+-- @field OVERCAST Overcast
+CLOUD_COVERAGE      = {}
+
+-------------------------------------------------------------------------------
+-- Could type table.
+-- Values from this table are used in the result table with key <code>clouds[n].type</code> returned by
+-- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
+-- @class table
+-- @name CLOUD_TYPE
+-- @field CUMULONIMBUS Cumulonimbus clouds
+-- @field TOWERING_CUMULUS Towering Cumulus clouds
+CLOUD_TYPE          = {}
+
+-------------------------------------------------------------------------------
+-- Could type table.
+-- Values from this table are used in the result table with key <code>sky</code> returned by
+-- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
+-- @class table
+-- @name SKY_STATUS
+-- @field UNKNOWN Sky type is unknown
+-- @field OBSCURE Obscured sky
+-- @field CLOUDS Clouds in the sky
+-- @field CLEAR Clear sky
+-- @field NO_SIGNIFICANT_CLOUDS No significant clouds detected
+-- @field NO_CLOUDS_DETECTED No clouds detected
+SKY_STATUS          = { UNKNOWN = 1, OBSCURE = 2, CLOUDS = 3}
+
+-------------------------------------------------------------------------------
+-- Weather intensity table.
+-- Values from this table are used in the result table with key <code>weather.intensity</code> returned by
+-- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
+-- @class table
+-- @name WEATHER_INTENSITY
+-- @field MODERATE Moderate phenomena
+-- @field LIGHT Light phenomena
+-- @field HEAVY Heavy phenomena
+-- @field VICINITY In the vicinity of the weather observation point
+WEATHER_INTENSITY   = { MODERATE = 1 }
+
+-------------------------------------------------------------------------------
+-- Weather descriptor table.
+-- Values from this table are used in the result table with key <code>weather.descriptor</code> returned by
+-- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
+-- @class table
+-- @name WEATHER_DESCRIPTOR
+-- @field SHALLOW Shallow phenomena
+-- @field PARTIAL Partial phenomena
+-- @field PATCHES Patches phenomena
+-- @field DRIFTING Drifring phenomena
+-- @field BLOWING Blowing phenomena
+-- @field SHOWERS Showers phenomena
+-- @field THUNDERSTORM Thunderstorm phenomena
+-- @field FREEZING Freezing phenomena
+WEATHER_DESCRIPTOR   = {}
+
+-------------------------------------------------------------------------------
+-- Weather phenomena table.
+-- Values from this table are used in the result table with key <code>weather.phenomena</code> returned by
+-- <a href="#metatable.__index:get_metar_data"><code>get_metar_data()</code></a>.
+-- @class table
+-- @name WEATHER_PHENOMENA
+-- @field DRIZZLE Drizzle
+-- @field RAIN Rain
+-- @field SNOW Snow
+-- @field SNOW_GRAINS Snow grains
+-- @field ICE_CRYSTALS Ice crystals
+-- @field ICE_PELLETS Ice pellets
+-- @field HAIL Hail
+-- @field SMALL_HAIL Small hail
+-- @field UNKNOWN Unknown phenomena
+-- @field MIST Mist
+-- @field FOG Fog
+-- @field SMOKE Smoke
+-- @field VOLCANIC_ASH Volcanic ash
+-- @field WIDESPREAD_DUST Widespread dust
+-- @field SAND Sand
+-- @field HAZE Haze
+-- @field SPRAY Spray
+-- @field DUST_WHIRLS Dust whirls
+-- @field SQUALLS Squalls
+-- @field FUNNEL_CLOUD Funnel cloud
+-- @field SAND_STORM Sand storm
+-- @field DUST_STORM Dust storm
+WEATHER_PHENOMENA   = {}
+
+-- Fill the constant tables
+for i, key in pairs(token_data.wind.directions) do
+       WIND_DIRECTION[key.key] = i + token_data.wind.offset
+end
+for i, key in pairs(token_data.clouds.coverages) do
+       CLOUD_COVERAGE[key.key] = i
+end
+for i, key in pairs(token_data.clouds.types) do
+       CLOUD_TYPE[key.key] = i
+end
+for i, key in pairs(token_data.sky.types) do
+       SKY_STATUS[key.key] = i + token_data.sky.offset
+end
+for i, key in pairs(token_data.weather_intensity.types) do
+       WEATHER_INTENSITY[key.key] = i + token_data.weather_intensity.offset
+end
+for i, key in pairs(token_data.weather_descriptor.types) do
+       WEATHER_DESCRIPTOR[key.key] = i
+end
+for i, key in pairs(token_data.weather_phenomena.types) do
+       WEATHER_PHENOMENA[key.key] = i
+end
+
+local function parse_metar_date(day, hour, min)
+       if not day or not hour or not min then
+               return
+       end
+       day  = tonumber(day)
+       hour = tonumber(hour)
+       min  = tonumber(min)
+       local now = os.date('!*t')
+       if day > now.day and now.day == 1 then
+               now.month = _get_previous_month(now)
+               now.day = _get_last_day_of_month(now)
+               if now.month == 12 then
+                       now.year = now.year - 1
+               end
+       else
+               now.day = day
+       end
+       now.hour = hour
+       now.min  = min
+       now.sec  = 0
+       return { timestamp = os.time(now) }
+end
+
+-- The parse_* routines parse snippets of METAR data
+
+local function parse_metar_wind(dir, speed, gust, unit)
+       local direction
+       if gust and token_data.wind.speeds[gust] then
+                       unit = gust
+                       gust = nil
+       end
+       if not dir or not speed or not unit or not token_data.wind.speeds[unit] then
+               return
+       end
+       if dir:match('^%d%d%d$') then
+               dir = tonumber(dir)
+               for _, test_direction in pairs(token_data.wind.directions) do
+                       if (
+                                                       (test_direction.min > test_direction.max)
+                                               and
+                                                       (test_direction.min <= dir or dir <= test_direction.max)
+                                       or
+                                                       (test_direction.max > test_direction.min)
+                                               and
+                                                       (test_direction.min <= dir and dir <= test_direction.max)
+                                       ) then
+                               direction = WIND_DIRECTION[test_direction.key]
+                               break
+                       end
+               end
+       else
+               direction = WIND_DIRECTION[dir]
+       end
+       if not direction then
+               return
+       end
+       local data = { direction = direction, speed = weatherlib.convert_speed(token_data.wind.speeds[unit], weatherlib.SPEED_UNITS.KNOT, tonumber(speed)) }
+       if gust ~= nil then
+               data['gust'] = weatherlib.convert_speed(token_data.wind.speeds[unit], weatherlib.SPEED_UNITS.KNOT, tonumber(gust))
+       end
+       return { wind = data }
+end
+
+local function parse_metar_visibility(visibility, direction)
+       if visibility == 'CAVOK' then
+               visibility = 9999
+       end
+       local data = { distance = tonumber(visibility) }
+       if direction and direction:len() > 0 then
+               if WIND_DIRECTION[direction] then
+                       data['direction'] = WIND_DIRECTION[direction]
+               else
+                       return
+               end
+       end
+       return { visibility = data }
+end
+
+local function parse_metar_runway_visual_range(runway, visibility)
+       return { runway_visual_range = { runway = tonumber(runway), visibility = tonumber(visibility) } } 
+end
+
+local function parse_metar_clouds(coverage, altitude, type)
+       local coverage_key
+       for _, coverage_data in pairs(token_data.clouds.coverages) do
+               if coverage == coverage_data.token or coverage == coverage_data.alias then
+                       coverage_key = coverage_data.key
+                       break
+               end
+       end
+       if not coverage_key then
+               return
+       end
+       if not CLOUD_COVERAGE[coverage_key] then
+               return
+       end
+       local data = { coverage = CLOUD_COVERAGE[coverage_key], altitude = tonumber(altitude) }
+       if type and type:len() > 0 then
+               local type_key
+               for _, type_data in pairs(token_data.clouds.types) do
+                       if type == type_data.token then
+                               type_key = type_data.key
+                               break
+                       end
+               end
+               if not type_key then
+                       return
+               end
+               if not CLOUD_TYPE[type_key] then
+                       return
+               end
+               data['type'] = CLOUD_TYPE[type_key]
+       end
+       return { clouds = data }
+end
+
+local function parse_metar_vertical_visibility(visibility)
+       if visibility and visibility:find('^%d%d%d$') then
+               visibility = tonumber(visibility)
+       elseif visibility == '///' then
+               visibility = 0
+       else
+               return
+       end
+       return { vertical_visibility = visibility }
+end
+
+local function parse_metar_sky(status)
+       local sky_key
+       for _, sky_data in pairs(token_data.sky.types) do
+               if status == sky_data.token or status == sky_data.alias then
+                       sky_key = sky_data.key
+                       break
+               end
+       end
+       if not sky_key then
+               return
+       end
+       if not SKY_STATUS[sky_key] then
+               return
+       end
+       return { sky = SKY_STATUS[sky_key] }
+end
+
+local function parse_metar_weather(intensity, descriptor, phenomena)
+       if not phenomena or phenomena:len() == 0 then
+               phenomena = descriptor
+               descriptor = nil
+       end
+       local intensity_key, descriptor_key, phenomena_key
+       if not intensity or intensity:len() == 0 then
+               intensity = WEATHER_INTENSITY.MODERATE
+       else
+               for _, intensity_data in pairs(token_data.weather_intensity.types) do
+                       if intensity == intensity_data.token then
+                               intensity_key = intensity_data.key
+                               break
+                       end
+               end
+               if not intensity_key or not WEATHER_INTENSITY[intensity_key] then
+                       return
+               end
+               intensity = WEATHER_INTENSITY[intensity_key]
+       end
+       if descriptor and descriptor:len() > 0 then
+               for _, descriptor_data in pairs(token_data.weather_descriptor.types) do
+                       if descriptor == descriptor_data.token then
+                               descriptor_key = descriptor_data.key
+                               break
+                       end
+               end
+               if not descriptor_key or not WEATHER_DESCRIPTOR[descriptor_key] then
+                       return
+               end
+               descriptor = WEATHER_DESCRIPTOR[descriptor_key]
+       end
+       if not phenomena or phenomena:len() == 0 then
+               return
+       end
+       for _, phenomena_data in pairs(token_data.weather_phenomena.types) do
+               if phenomena == phenomena_data.token or phenomena == phenomena_data.alias then
+                       phenomena_key = phenomena_data.key
+                       break
+               end
+       end
+       if not phenomena_key or not WEATHER_PHENOMENA[phenomena_key] then
+               return
+       end
+       phenomena = WEATHER_PHENOMENA[phenomena_key]
+       local weather_data = { intensity = intensity, phenomena = phenomena }
+       if descriptor then
+               weather_data['descriptor'] = descriptor
+       end
+       return { weather = weather_data }
+end
+
+local function parse_metar_temperature(temperature, dewpoint)
+       temperature = temperature:gsub('^M', '-')
+       dewpoint = dewpoint:gsub('^M', '-')
+       return { temperature = tonumber(temperature), dewpoint = tonumber(dewpoint) }
+end
+
+local function parse_metar_pressure(unit, pressure)
+       if unit == 'A' then
+               pressure = weatherlib.convert.pressure[weatherlib.PRESSURE_UNITS.INHG][weatherlib.PRESSURE_UNITS.HPA](tonumber(pressure) / 100)
+       else
+               pressure = tonumber(pressure)
+       end
+       return { pressure = pressure }
+end
+
+-- Define patterns for each snippet of METAR data to parse
+local metar_token_handlers = {
+       {
+               pattern = '^(%d%d)(%d%d)(%d%d)Z$',
+               handler = parse_metar_date,
+       },
+       {
+               pattern = '^(%w%w%w)(%d+)G(%d+)([A-Z]+)$',
+               handler = parse_metar_wind,
+       },
+       {
+               pattern = '^(%w%w%w)(%d+)([A-Z]+)$',
+               handler = parse_metar_wind,
+       },
+       {
+               pattern = '^(%d%d%d%d)([A-Z]*)$',
+               handler = parse_metar_visibility,
+       },
+       {
+               pattern = '^(CAVOK)$',
+               handler = parse_metar_visibility,
+       },
+       {
+               pattern = '^R(%d+)/(%d+)$',
+               handler = parse_metar_runway_visual_range,
+       },
+       {
+               pattern = '^([A-Z][A-Z][A-Z])(%d%d%d)([A-Z]*)$',
+               handler = parse_metar_clouds,
+       },
+       {
+               pattern = '^VV([%d/][%d/][%d/])$',
+               handler = parse_metar_vertical_visibility,
+       },
+       {
+               pattern = '^([A-Z]+)$',
+               handler = parse_metar_sky,
+       },
+       {
+               pattern = '^([+-]*)([A-Z][A-Z])$',
+               handler = parse_metar_weather,
+       },
+       {
+               pattern = '^([+-]*)([A-Z][A-Z])([A-Z][A-Z])$',
+               handler = parse_metar_weather,
+       },
+       {
+               pattern = '^(VC)([A-Z][A-Z])*$',
+               handler = parse_metar_weather,
+       },
+       {
+               pattern = '^(VC)([A-Z][A-Z])([A-Z][A-Z])*$',
+               handler = parse_metar_weather,
+       },
+       {
+               pattern = '^(M*%d+)/(M*%d+)$',
+               handler = parse_metar_temperature,
+       },
+       {
+               pattern = '^([QA])(%d+)$',
+               handler = parse_metar_pressure,
+       },
+}
+
+local function _get_previous_month(date)
+       if date.month == 1 then
+               return 12
+       end
+       return date.month - 1
+end
+
+local function _get_last_day_of_month(date)
+       date.day  = 1
+       date.hour = 0
+       date.min  = 0
+       date.sec  = 0
+       date.wday = 0
+       date.yday = 0
+       local day_seconds = 60 * 60 * 24
+       local timestamp = os.time(date)
+       local test_date = date
+       -- count days forward until month changes
+       while(date.month == test_date.month) do
+               timestamp = os.time(test_date) + day_seconds
+               test_date = os.date('*t', timestamp)
+       end
+       -- rewind one day to get the last day of the month
+       timestamp = timestamp - day_seconds
+       return os.date('*t', timestamp).day
+end
+
+local metatable = { __index = {} }
+
+-------------------------------------------------------------------------------
+-- Create a new METAR object
+-- @param args String that is either the METAR data string (one line) to parse
+-- or the four-letter, upper-case <a href="http://en.wikipedia.org/wiki/International_Civil_Aviation_Organization_airport_code">ICAO code</a>
+-- for the weather station. If weather station code is given, the current
+-- METAR data for the station is downloaded from <a href="http://weather.noaa.gov">IWS</a>.
+-- @return A table which is the metar object for METAR data given or
+-- downloaded from IWS for the given weather station code
+function new(args)
+       local attributes = {}
+       if args and type(args) == 'string' then
+               if args:find('^[A-Z][A-Z][A-Z][A-Z]$') then
+                       attributes.station_id = args
+               else
+                       attributes.metar_string = args
+               end
+       end
+       return setmetatable(attributes, metatable)
+end
+
+-------------------------------------------------------------------------------
+-- Return parsed METAR data as a table
+-- @return Table containing the data parsed from the METAR data. 
+-- If an error occurs, returns nil as the first return value.
+-- The table may contain following entries
+-- <ul>
+-- <li><code>timestamp</code> <code>os.time</code> table which represents the timestamp when the METAR data was generated. Time is in UTC. Always included.</li>
+-- <li><code>wind</code> A table representing the wind phenomena with the following keys. Optional, but usually included.</li>
+--     <ul>
+--     <li><code>direction</code> Wind direction as a value of the <a href="#WIND_DIRECTION">WIND_DIRECTION</a> table.</li>
+--     <li><code>speed</code> Wind speed in knots.</li>
+--     <li><code>gust</code> Gust speed in knots, optional.</li>
+--     </ul>
+-- <li><code>visibility</code> A list of tables that represent the visibility towards different directions. Tables contain the following keys. Optional, but if defined, at least one visibility entry exists in the list. Usually included.</li>
+--     <ul>
+--     <li><code>direction</code> Direction as a value of the <a href="#WIND_DIRECTION">WIND_DIRECTION</a> table. Optional.</li>
+--     <li><code>distance</code> Visibility distance in meters</li>
+--     </ul>
+-- <li><code>vertical_visibility</code> Vertical visibility in meters. Optional.</li>
+-- <li><code>runway_visual_range</code> A table representing runway visual range with the following keys. Optional.</li>
+--     <ul>
+--     <li><code>runway</code> Runway code</li>
+--     <li><code>visibility</code> Visibility in meters</li>
+--     </ul>
+-- <li><code>clouds</code> A list of tables that represent clouds at different altitudes. Tables contain the following keys. Optional, but if defined, at least one cloud entry exists in the list. Usually included.</li>
+--     <ul>
+--     <li><code>coverage</code> Cloud coverate as a value of the <a href="#CLOUD_COVERAGE">CLOUD_COVERAGE</a> table.</li>
+--     <li><code>altitude</code> Altitude of the clouds in feet.</li>
+--     <li><code>type</code> Cloud type as a value of the <a href="#CLOUD_TYPE">CLOUD_TYPE</a> table.</li>
+--     </ul>
+-- <li><code>weather</code> A table representing weather conditions with the following keys. Optional, but usually included.</li>
+--     <ul>
+--     <li><code>intensity</code> Weather intensity as a value of the <a href="#WEATHER_INTENSITY">WEATHER_INTENSITY</a> table. Optional.</li></li>
+--     <li><code>descriptor</code> Weather descriptor as a value of the <a href="#WEATHER_DESCRIPTOR">WEATHER_DESCRIPTOR</a> table. Optional.</li>
+--     <li><code>phenomena</code> Weather phenomena as a value of the <a href="#WEATHER_PHENOMENA">WEATHER_PHENOMENA</a> table. Always included.</li>
+--     </ul>
+-- <li><code>sky</code> Sky status as a value of the <a href="#SKY_STATUS">SKY_STATUS</a> table. Always included.</li>
+-- <li><code>temperature</code> Temperature in Celcius. Always  included.</li>
+-- <li><code>dewpoint</code> Dewpoint temperature in Celcius. Always included.</li>
+-- <li><code>pressure</code> Pressure in hectopascals. Optional, but usually included.</li>
+--</ul>
+-- @return Error string in case an error occurred and nil METAR table is returned
+-- @usage var m = metar.new('EFHF')          -- Weather station Helsinki/Malmi
+-- @usage var md = m:get_metar_data()        -- metardata.temperature contains the temperature etc.
+-- @usage if md.temperature >= 30 then print("It's hot!") end
+-- @usage if md.weather.intensity and md.weather.intensity == m.WEATHER_INTENSITY.HEAVY and md.weather.phenomena and md.weather.phenomena == m.WEATHER_PHENOMENA.RAIN then print("It's raining a lot!") end
+function metatable.__index:get_metar_data()
+       if self.station_id then
+               local metar_string, error_string = self:_fetch_metar_string(self.station_id)
+               if not metar_string then
+                       return nil, error_string
+               end
+               self.metar_string = metar_string
+       end
+       if not self.metar_string then
+               return nil, 'No METAR string or station id'
+       end
+       local metar_data = self:_parse_metar_string(self.metar_string)
+       if self.station_id then
+               metar_data['station'] = self.station_id
+       end
+       metar_data['metar_string'] = self.metar_string
+       return metar_data
+end
+
+function metatable.__index:_parse_metar_string(metar_string)
+       local multi_entry = { visibility = 1, runway_visual_range = 1, clouds = 1 }
+       local metar_data  = {}
+       local token_data
+       -- tokenize METAR data
+       metar_string:gsub('%S+', function(token)
+                       local data = {}
+                       -- test each parser against the token and
+                       -- if it matches, pass to the handler
+                       for _, token_handler in pairs(metar_token_handlers) do
+                               token:gsub(token_handler.pattern,
+                                       function(...)
+                                               local value = token_handler.handler(...)
+                                               table.insert(data, value)
+                                       end)
+                       end
+                       -- save results
+                       for _, data_entry in pairs(data) do
+                               if data_entry then
+                                       for key, value in pairs(data_entry) do
+                                               if multi_entry[key] then
+                                                       if not metar_data[key] then
+                                                               metar_data[key] = {}
+                                                       end
+                                                       table.insert(metar_data[key], value)
+                                               else
+                                               metar_data[key] = value
+                                               end
+                                       end
+                               end
+                       end
+       end)
+       -- METAR data parsed, do some post processing
+       if metar_data.clouds then
+               metar_data.sky = SKY_STATUS.CLOUDS
+       elseif metar_data.vertical_visibility then
+               metar_data.sky = SKY_STATUS.OBSCURE
+       end
+       if not metar_data.sky then
+               metar_data.sky = SKY_STATUS.UNKNOWN
+       end
+       return metar_data
+end
+
+-- Download and validate METAR string for a weather station
+function metatable.__index:_fetch_metar_string(station_id)
+       local body, error_string = self:_download_metar_file(station_id)
+       if not body then
+               return nil, error_string
+       end
+       local metar_string
+       local metar_pattern = string.format('^%s%%s+%%d%%d%%d%%d%%d%%dZ%%s+', self.station_id)
+       body:gsub('(.-)\n', function (line)
+                       if line:find(metar_pattern) then
+                               metar_string = line
+                               return
+                       end
+       end)
+       if metar_string then
+               return metar_string
+       else
+               return nil, 'Failed to find METAR string from input'
+       end
+end
+
+-- Download the METAR data for a weather station from IWS
+function metatable.__index:_download_metar_file(station_id)
+       local metar_url = string.format('http://weather.noaa.gov/pub/data/observations/metar/stations/%s.TXT', station_id)
+       local body, status, _, status_string = socket_http.request(metar_url)
+       local error_details
+       if not status or (type(status) ~= 'number' and type(status) ~= 'string') then
+               error_details = 'Unknown error'
+       elseif type(status) == 'string' then
+               error_details = status
+       elseif type(status) == 'number' and status ~= 200 then
+               error_details = status_string
+       end
+       if error_details then
+               local error_string = string.format('Failed to fetch METAR data for station %s: %s', self.station_id, error_details)
+               self:log_error(error_string)
+               return nil, error_string
+       end
+       return body
+end
+
+function metatable.__index:log_error(error_string)
+       print(string.format('[metar] %s', error_string))
+end