Repository: python-metar/python-metar

Issue:
Add support for Ice Group IXnnn
A NWS collaborator reached out to me wondering if I was parsing the I Group for 1,3 hourly ice accumulation totals.  These are now collected from new sensors in Texas and Oklahoma.  For example:

```
KABI 031752Z 30010KT 6SM BR FEW009 OVC036 02/01 A3003 RMK AO2 SLP176 60001 I6003 T00170006 10017 21006 56017
```

From FMH-1
>Ice Accretion. At designated stations (with SPECI capability freezing rain sensor) ice
accretion shall be reported in METAR and SPECI reports when accretion is occurring or
has occurred during the reporting period.
>1. Hourly Ice Accretion Amount (I1nnn). At designated automated stations, the hourly
ice accretion amount shall be coded in the format I1nnn, where I is the ice accretion
group indicator, 1 indicates an hourly amount, and nnn is the accretion amount since
the last METAR. The amount shall be coded in hundredths of an inch. For example,
"I1004" would indicate 4/100 of an inch of ice accretion in the past hour.
>2. 3- and 6-Hourly Ice Accretion Amount (I3nnn and I6nnn). At designated stations,
the 3- and 6-hourly ice accretion group shall be coded in the format I3nnn and I6nnn,
respectively, where I is the group indicator, 3 and 6 indicate 3- and 6- hourly groups,
respectively, and nnn is the accretion amount. The amount of ice accretion in the past
3 hours shall be reported in the 3-hourly report; the amount of ice accretion in the past
6 hours shall be reported in the 6-hourly report. The amount shall be coded in
hundredths of an inch. For example, I6023 would indicate 23/100 of an inch of ice
accretion in the past 6 hours.


Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/python-metar/python-metar/pull/78?src=pr&el=h1) Report
> Merging [#78](https://codecov.io/gh/python-metar/python-metar/pull/78?src=pr&el=desc) into [master](https://codecov.io/gh/python-metar/python-metar/commit/47bde0ea2224b197cfcbcb5d500e8ae761837e87?src=pr&el=desc) will **increase** coverage by `0.65%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/python-metar/python-metar/pull/78/graphs/tree.svg?width=650&token=W7CMdTTvRY&height=150&src=pr)](https://codecov.io/gh/python-metar/python-metar/pull/78?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #78      +/-   ##
==========================================
+ Coverage   87.32%   87.98%   +0.65%     
==========================================
  Files           4        4              
  Lines        1018     1032      +14     
==========================================
+ Hits          889      908      +19     
+ Misses        129      124       -5
```


| [Impacted Files](https://codecov.io/gh/python-metar/python-metar/pull/78?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [metar/Metar.py](https://codecov.io/gh/python-metar/python-metar/pull/78/diff?src=pr&el=tree#diff-bWV0YXIvTWV0YXIucHk=) | `90.46% <100%> (+0.83%)` | :arrow_up: |
| [metar/Datatypes.py](https://codecov.io/gh/python-metar/python-metar/pull/78/diff?src=pr&el=tree#diff-bWV0YXIvRGF0YXR5cGVzLnB5) | `84.61% <0%> (+0.28%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-metar/python-metar/pull/78?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-metar/python-metar/pull/78?src=pr&el=footer). Last update [47bde0e...f60cd35](https://codecov.io/gh/python-metar/python-metar/pull/78?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

coveralls: ## Pull Request Test Coverage Report for [Build 85](https://coveralls.io/builds/21067809)

* **14** of **14**   **(100.0%)**  changed or added relevant lines in **1** file are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage increased (+**0.7%**) to **87.984%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [metar/Metar.py](https://coveralls.io/builds/21067809/source?filename=metar%2FMetar.py#L519) | 1 | 90.46% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/21067809/badge)](https://coveralls.io/builds/21067809) |
| :-- | --: |
| Change from base [Build 84](https://coveralls.io/builds/19618672): |  0.7% |
| Covered Lines: | 908 |
| Relevant Lines: | 1032 |

---
##### 💛  - [Coveralls](https://coveralls.io)

akrherz: looking at this data more closely, it appears to follow the same precipitation nomenclature such that `000` represents a TRACE value.  Gonna seek some confirmation on this first, so please hold of on merging for the moment.

Patch:
diff --git a/metar/Datatypes.py b/metar/Datatypes.py
index c9eb438..9f92ced 100644
--- a/metar/Datatypes.py
+++ b/metar/Datatypes.py
@@ -376,8 +376,8 @@ class precipitation(object):
             raise ValueError("unrecognized greater-than/less-than symbol: '"+gtlt+"'")
         self._gtlt = gtlt
         self._value = float(value)
-        # In METAR world, a string of four zeros denotes trace
-        self._istrace = (value == "0000")
+        # In METAR world, a string of just four or three zeros denotes trace
+        self._istrace = (value in ["0000", "000"])
 
     def __str__(self):
         return self.string()
diff --git a/metar/Metar.py b/metar/Metar.py
index c8274e5..7275b2a 100644
--- a/metar/Metar.py
+++ b/metar/Metar.py
@@ -144,6 +144,9 @@ TS_LOC_RE = re.compile(r"""TS(\s+(?P<loc>( OHD | VC | DSNT\s+ | \s+AND\s+ |
                                           ( \s+MOV\s+(?P<dir>[NSEW][EW]?) )?\s+""",
                            re.VERBOSE)
 SNOWDEPTH_RE = re.compile(r"""^4/(?P<snowdepth>\d\d\d)\s+""")
+ICE_ACCRETION_RE = re.compile(
+    r"^I(?P<ice_accretion_hours>[136])(?P<ice_accretion_depth>\d\d\d)\s+"
+)
 
 
 # translation of weather location codes
@@ -345,6 +348,9 @@ class Metar(object):
         self.precip_6hr = None             # precipitation over the last 6 hours
         self.precip_24hr = None            # precipitation over the last 24 hours
         self.snowdepth = None              # snow depth (distance)
+        self.ice_accretion_1hr = None      # ice accretion over the past hour
+        self.ice_accretion_3hr = None      # ice accretion over the past 3 hours
+        self.ice_accretion_6hr = None      # ice accretion over the past 6 hours
         self._trend = False                # trend groups present (bool)
         self._trend_groups = []            # trend forecast groups
         self._remarks = []                 # remarks (list of strings)
@@ -924,6 +930,14 @@ class Metar(object):
         self.snowdepth = distance(float(d['snowdepth']), 'IN')
         self._remarks.append(" snowdepth %s" % (self.snowdepth, ))
 
+    def _handleIceAccretionRemark(self, d):
+        """
+        Parse the I/ group ice accretion report.
+        """
+        myattr = "ice_accretion_%shr" % (d['ice_accretion_hours'], )
+        value = precipitation(float(d['ice_accretion_depth']) / 100., 'IN')
+        setattr(self, myattr, value)
+
     def _unparsedRemark( self, d ):
         """
         Handle otherwise unparseable remark groups.
@@ -974,6 +988,7 @@ class Metar(object):
                         (TEMP_6HR_RE,     _handleTemp6hrRemark),
                         (TEMP_24HR_RE,    _handleTemp24hrRemark),
                         (SNOWDEPTH_RE,    _handleSnowDepthRemark),
+                        (ICE_ACCRETION_RE, _handleIceAccretionRemark),
                         (UNPARSED_RE,     _unparsedRemark) ]
 
     # functions that return text representations of conditions for output
@@ -1026,6 +1041,12 @@ class Metar(object):
             lines.append("6-hour precipitation: %s" % str(self.precip_6hr))
         if self.precip_24hr:
             lines.append("24-hour precipitation: %s" % str(self.precip_24hr))
+        if self.ice_accretion_1hr:
+            lines.append("1-hour Ice Accretion: %s" % str(self.ice_accretion_1hr, ))
+        if self.ice_accretion_3hr:
+            lines.append("3-hour Ice Accretion: %s" % str(self.ice_accretion_3hr, ))
+        if self.ice_accretion_6hr:
+            lines.append("6-hour Ice Accretion: %s" % str(self.ice_accretion_6hr, ))
         if self._remarks:
             lines.append("remarks:")
             lines.append("- "+self.remarks("\n- "))


Test patch:
diff --git a/test/test_metar.py b/test/test_metar.py
index 6a0e6ca..0d99861 100644
--- a/test/test_metar.py
+++ b/test/test_metar.py
@@ -39,6 +39,18 @@ def test_module():
     assert hasattr(metar, "__version__")
 
 
+@pytest.mark.parametrize("hours", [1, 3, 6])
+def test_issue77_ice_accretion(hours):
+    """Metar parser supports ice accretion data."""
+    report = Metar.Metar(
+        ("KABI 031752Z 30010KT 6SM BR FEW009 OVC036 02/01 A3003 RMK AO2 "
+         "SLP176 60001 I%i003 T00170006 10017 21006 56017") % (hours, )
+    )
+    myattr = "ice_accretion_%ihr" % (hours, )
+    assert abs(getattr(report, myattr).value('IN') - 0.03) < 0.001
+    assert str(report).find("Ice Accretion") > 0
+
+
 def test_issue64_cloudkeyerror():
     """Lookup on CLOUD_TYPE should not keyerror."""
     report = Metar.Metar(